Skip to content
Snippets Groups Projects
Commit b8c700d2 authored by 한동현's avatar 한동현
Browse files

feat(c): 카메라 컨트롤 노드

parents
No related branches found
No related tags found
No related merge requests found
# 빌드 파일
*.o
camera
# 이미지 폴더
images/
plates/
# IDE 관련 파일
.vscode/
*~
# 시스템 파일
.DS_Store
.directory
\ No newline at end of file
#include "gpio.h"
#include <chrono>
#include <iostream>
#include <memory>
#include <thread>
#include <vector>
#include <queue>
#include <string>
#include <fstream>
#include <sys/mman.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <libcamera/libcamera.h>
#include <opencv2/opencv.hpp>
#include <iomanip>
#include <stdexcept>
#include <cmath>
#include <arpa/inet.h>
using namespace std;
using namespace libcamera;
using namespace GPIO;
#define BUFFER_SIZE 1024
#define POUT 535 // 초음파 트리거 핀
#define PIN 536 // 초음파 에코 핀
// 번호판 후보 영역 필터링을 위한 파라미터
const int MIN_AREA = 100;
const int MIN_WIDTH = 4;
const int MIN_HEIGHT = 2;
const float MIN_RATIO = 0.25f;
const float MAX_RATIO = 0.9f;
// 번호판 문자 검출 파라미터
const float MAX_DIAG = 5.0f;
const float MAX_ANGLE = 12.0f;
const float MAX_AREA_DIFF = 0.6f;
const float MAX_WIDTH_DIFF = 0.8f;
const float MAX_HEIGHT_DIFF = 0.2f;
const int MIN_CHARACTER = 6;
// 번호판 문자 여백 비율
const float MARGIN_RATIO = 0.1f;
// 파일 저장 경로 (폴더 생성 필요)
const string IMAGES_DIR = "../images";
const string PLATES_DIR = "../plates";
struct Rect {
int x, y, w, h;
float cx, cy;
Rect(int x, int y, int w, int h, float cx, float cy) : x(x), y(y), w(w), h(h), cx(cx), cy(cy) {}
bool operator==(const Rect& other) const {
return x == other.x && y == other.y && w == other.w && h == other.h && cx == other.cx && cy == other.cy;
}
};
int server_socket, python_socket; // 서버 소켓 및 파이썬 소켓
bool detected = false; // 차량 감지 여부
bool fireAlert = false; // 화재 경보 여부
pthread_t sensor_thread, receive_thread;
shared_ptr<Camera> camera;
unique_ptr<CameraManager> cm;
unique_ptr<Request> request;
// 서버 메시지 수신 스레드
void* receive_message(void* arg) {
char buffer[BUFFER_SIZE];
while (true) {
auto read_bytes = read(server_socket, buffer, BUFFER_SIZE - 1);
if (read_bytes <= 0) {
cout << "서버와의 연결이 종료되었습니다." << endl;
close(server_socket);
exit(0);
}
buffer[read_bytes] = '\0';
cout << "서버로부터 받은 메시지: " << buffer << endl;
string msg(buffer);
msg = msg.substr(0, msg.find_first_of("\r\n"));
if (msg == "0-FIRE") {
fireAlert = true;
cout << "화재 경보가 발생했습니다." << endl;
} else if (msg == "0-CLEAR") {
fireAlert = false;
cout << "화재 경보가 해제되었습니다." << endl;
}
}
return nullptr;
}
// 초음파 센서 측정 스레드
void* ultrasonic_sensor(void *arg) {
clock_t start_t, end_t;
while (1) {
// 초음파 센서 측정
GPIOWrite(POUT, Value::HIGH);
usleep(10);
GPIOWrite(POUT, Value::LOW);
// 에코 핀 신호 대기
while (GPIORead(PIN) == 0) start_t = clock();
while (GPIORead(PIN) == 1) end_t = clock();
double time = (double) (end_t - start_t) / CLOCKS_PER_SEC; // 시간 계산
double distance = time / 2 * 34000; // cm 단위 거리 계산
cout << distance << "cm" << endl;
if (!detected && distance <= 10) { // 10cm 이하 -> 차량 접근
detected = true;
cout << "차량 감지: " << distance << "cm" << endl;
// 카메라를 시작합니다.
if (camera->start()) {
throw runtime_error("Failed to start camera");
}
// 카메라 촬영
if (camera->queueRequest(request.get()) < 0) {
throw runtime_error("Failed to queue request");
}
this_thread::sleep_for(chrono::milliseconds(2000));
// 카메라 종료
camera->stop();
} else if (detected && distance > 10) { // 10cm 초과 -> 차량 없음
detected = false;
cout << "차량 감지 해제" << endl;
}
usleep(500000); // 0.5초 주기
}
return nullptr;
}
vector<Rect> detect_character(const vector<Rect>& rects) {
for (size_t idx = 0; idx < rects.size(); idx++) {
vector<Rect> match = {rects[idx]};
queue<size_t> queue;
queue.push(idx);
while (!queue.empty()) {
size_t cur = queue.front(); queue.pop();
const Rect& r1 = rects[cur];
for (size_t nxt = 0; nxt < rects.size(); nxt++) {
if (cur == nxt || find(match.begin(), match.end(), rects[nxt]) != match.end()) continue;
const Rect& r2 = rects[nxt];
float dx = abs(r1.cx - r2.cx);
float dy = abs(r1.cy - r2.cy);
float diagonal = sqrt(r1.w * r1.w + r1.h * r1.h);
float distance = hypot(dx, dy);
float angle = dx != 0 ? abs(atan2(dy, dx) * 180.0f / CV_PI) : 90.0f;
float diff_area = abs(r1.w * r1.h - r2.w * r2.h) / float(r1.w * r1.h);
float diff_width = abs(r1.w - r2.w) / float(r1.w);
float diff_height = abs(r1.h - r2.h) / float(r1.h);
if (distance < diagonal * MAX_DIAG && angle < MAX_ANGLE && diff_area < MAX_AREA_DIFF && diff_width < MAX_WIDTH_DIFF && diff_height < MAX_HEIGHT_DIFF) {
match.push_back(rects[nxt]);
queue.push(nxt);
}
}
}
if (match.size() >= MIN_CHARACTER) {
return match;
}
}
throw runtime_error("문자 검출 실패");
}
void detect_plate(const string& img_path) {
// 이미지 읽기
cv::Mat img = cv::imread(img_path);
if (img.empty()) {
cout << "이미지를 찾을 수 없습니다." << endl;
return;
}
cv::Mat orig = img.clone();
cv::Mat final_result = orig.clone();
// 그레이스케일 변환
cv::Mat gray;
cv::cvtColor(img, gray, cv::COLOR_BGR2GRAY);
// Morphology 연산
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3, 3));
cv::Mat tophat, blackhat, gray_enhanced;
cv::morphologyEx(gray, tophat, cv::MORPH_TOPHAT, kernel);
cv::morphologyEx(gray, blackhat, cv::MORPH_BLACKHAT, kernel);
cv::add(gray, tophat, gray_enhanced);
cv::subtract(gray_enhanced, blackhat, gray_enhanced);
// 가우시안 블러
cv::Mat blur;
cv::GaussianBlur(gray_enhanced, blur, cv::Size(5, 5), 2);
// 적응형 이진화
cv::Mat thresh;
cv::adaptiveThreshold(blur, thresh, 255, cv::ADAPTIVE_THRESH_MEAN_C, cv::THRESH_BINARY, 91, 3);
// 컨투어 찾기
vector<vector<cv::Point>> contours;
cv::findContours(thresh, contours, cv::RETR_LIST, cv::CHAIN_APPROX_SIMPLE);
vector<Rect> rects;
for (const auto& contour : contours) {
cv::Rect rect = cv::boundingRect(contour);
float cx = rect.x + rect.width / 2.0f;
float cy = rect.y + rect.height / 2.0f;
if (rect.width * rect.height > MIN_AREA && rect.width > MIN_WIDTH && rect.height > MIN_HEIGHT && MIN_RATIO < float(rect.width) / rect.height && float(rect.width) / rect.height < MAX_RATIO) {
rects.emplace_back(rect.x, rect.y, rect.width, rect.height, cx, cy);
}
}
try {
// 문자 컨투어 찾기
vector<Rect> chars = detect_character(rects);
// 여백 적용
vector<cv::Point2f> points;
for (const auto& char_rect : chars) {
int margin_w = int(char_rect.w * MARGIN_RATIO);
int margin_h = int(char_rect.h * MARGIN_RATIO);
points.emplace_back(char_rect.x - margin_w, char_rect.y - margin_h);
points.emplace_back(char_rect.x + char_rect.w + margin_w, char_rect.y - margin_h);
points.emplace_back(char_rect.x - margin_w, char_rect.y + char_rect.h + margin_h);
points.emplace_back(char_rect.x + char_rect.w + margin_w, char_rect.y + char_rect.h + margin_h);
cv::rectangle(final_result, cv::Point(char_rect.x, char_rect.y), cv::Point(char_rect.x + char_rect.w, char_rect.y + char_rect.h), cv::Scalar(255, 0, 0), 1);
}
// 좌상단, 우상단, 우하단, 좌하단 점 찾기
vector<cv::Point2f> box(4);
cv::Point2f* pts = &points[0];
size_t n_pts = points.size();
// 좌상단 (x+y가 최소)
box[0] = *min_element(pts, pts + n_pts, [](const cv::Point2f& a, const cv::Point2f& b) { return a.x + a.y < b.x + b.y; });
// 우상단 (-x+y가 최소)
box[1] = *min_element(pts, pts + n_pts, [](const cv::Point2f& a, const cv::Point2f& b) { return -a.x + a.y < -b.x + b.y; });
// 우하단 (-x-y 최소)
box[2] = *min_element(pts, pts + n_pts, [](const cv::Point2f& a, const cv::Point2f& b) { return -a.x - a.y < -b.x - b.y; });
// 좌하단 (x-y가 최소)
box[3] = *min_element(pts, pts + n_pts, [](const cv::Point2f& a, const cv::Point2f& b) { return a.x - a.y < b.x - b.y; });
// 전체 번호판 영역 표시
vector<vector<cv::Point>> box_contour = { {cv::Point(box[0]), cv::Point(box[1]), cv::Point(box[2]), cv::Point(box[3])} };
cv::drawContours(final_result, box_contour, 0, cv::Scalar(0, 0, 255), 2);
// 투시 변환을 위한 크기 계산
float width = cv::norm(box[1] - box[0]);
float height = cv::norm(box[3] - box[0]);
vector<cv::Point2f> dst_pts = {
cv::Point2f(0, 0),
cv::Point2f(width - 1, 0),
cv::Point2f(width - 1, height - 1),
cv::Point2f(0, height - 1)
};
cv::Mat M = cv::getPerspectiveTransform(box, dst_pts);
cv::Mat plate_img;
cv::warpPerspective(orig, plate_img, M, cv::Size(width, height));
// 이미지 파일명에서 타임스탬프 추출
size_t start = img_path.find("image_") + 6;
size_t end = img_path.find(".jpg");
string timestamp_str = img_path.substr(start, end - start);
string plate_filename = PLATES_DIR + "/plate_" + timestamp_str + ".jpg";
// 결과 저장
cv::imwrite(plate_filename, plate_img);
// 파이썬 노드로 번호판 이미지 전송
if (send(python_socket, plate_filename.c_str(), plate_filename.size(), 0) < 0) {
perror("파이썬 노드로 번호판 이미지 전송 실패");
}
char buffer[BUFFER_SIZE];
auto read_bytes = read(python_socket, buffer, BUFFER_SIZE - 1);
if (read_bytes >= 0) {
buffer[read_bytes] = '\0';
cout << "파이썬 노드로부터 받은 메시지: " << buffer << endl;
string message = "0-" + string(buffer);
send(server_socket, message.c_str(), message.size(), 0);
}
} catch (const exception& e) {
cerr << e.what() << endl;
}
}
void requestComplete(Request* request) {
if (request->status() == Request::RequestComplete) {
const FrameBuffer* buffer = request->buffers().begin()->second;
const FrameBuffer::Plane& plane = buffer->planes().front();
void* memory = mmap(nullptr, plane.length, PROT_READ, MAP_SHARED, plane.fd.get(), 0);
if (memory != MAP_FAILED) {
cv::Mat raw(800, 1600, CV_8UC2, memory);
cv::Mat bgr;
cv::cvtColor(raw, bgr, cv::COLOR_YUV2BGR_YUYV);
vector<unsigned char> jpeg_buffer;
vector<int> params = {cv::IMWRITE_JPEG_QUALITY, 90};
cv::imencode(".jpg", bgr, jpeg_buffer, params);
// 현재 시간 타임스탬프
auto now = chrono::system_clock::now();
auto now_time = chrono::system_clock::to_time_t(now);
stringstream timestamp;
timestamp << put_time(localtime(&now_time), "%Y%m%d_%H%M%S");
string timestamp_str = timestamp.str();
string image_filename = IMAGES_DIR + "/image_" + timestamp_str + ".jpg";
ofstream file(image_filename, ios::binary);
if (file) {
file.write(reinterpret_cast<char*>(jpeg_buffer.data()), jpeg_buffer.size());
file.close();
cout << "이미지 저장: " << image_filename << endl;
} else {
cerr << "이미지 저장 실패: " << image_filename << endl;
}
munmap(memory, plane.length);
detect_plate(image_filename);
}
request->reuse(Request::ReuseBuffers);
}
}
int main() {
try {
cm = make_unique<CameraManager>();
cm->start();
auto cameras = cm->cameras();
if (cameras.empty()) {
throw runtime_error("No cameras found!");
}
camera = cameras[0];
camera->acquire();
unique_ptr<CameraConfiguration> config = camera->generateConfiguration({StreamRole::StillCapture});
StreamConfiguration &streamConfig = config->at(0);
streamConfig.size = Size(1600, 900);
streamConfig.pixelFormat = formats::YUYV;
streamConfig.bufferCount = 1;
CameraConfiguration::Status status = config->validate();
if (status != CameraConfiguration::Valid) {
throw runtime_error("Camera configuration invalid");
}
camera->configure(config.get());
FrameBufferAllocator allocator(camera);
Stream *stream = streamConfig.stream();
allocator.allocate(stream);
camera->requestCompleted.connect(requestComplete);
const vector<unique_ptr<FrameBuffer>> &buffers = allocator.buffers(stream);
request = camera->createRequest();
if (request->addBuffer(stream, buffers.at(0).get()) < 0) {
throw runtime_error("Failed to add buffer to request");
}
struct sockaddr_in server_addr, python_addr;
// 서버 노드 소켓 생성
if ((server_socket = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("서버 노드 소켓 생성 실패");
return -1;
}
// 서버 노드 주소 설정
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(12345);
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
// 서버 노드 연결
if (connect(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("서버 노드 연결 실패");
return -1;
}
printf("서버 노드에 연결되었습니다.\n");
// 파이썬 노드 소켓 생성
if ((python_socket = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("파이썬 노드 소켓 생성 실패");
return -1;
}
// 파이썬 노드 주소 설정
python_addr.sin_family = AF_INET;
python_addr.sin_port = htons(3000);
python_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
// 파이썬 노드 연결
if (connect(python_socket, (struct sockaddr *) &python_addr, sizeof(python_addr)) < 0) {
perror("파이썬 노드 연결 실패");
return -1;
}
printf("파이썬 노드에 연결되었습니다.\n");
// GPIO 핀 설정
GPIOExport(POUT);
GPIOExport(PIN);
usleep(100000); // GPIO 설정 대기
// GPIO 핀 방향 설정
GPIODirection(POUT, Direction::OUT);
// 스레드 생성
pthread_create(&sensor_thread, NULL, ultrasonic_sensor, NULL);
pthread_create(&receive_thread, NULL, receive_message, NULL);
// 스레드 종료 대기
pthread_join(sensor_thread, NULL);
pthread_join(receive_thread, NULL);
// 자원 해제
close(server_socket);
close(python_socket);
GPIOUnexport(POUT);
GPIOUnexport(PIN);
camera->requestCompleted.disconnect();
camera->stop();
camera->release();
allocator.free(stream);
cm->stop();
} catch (const exception& e) {
cerr << "Error: " << e.what() << endl;
return 1;
}
return 0;
}
#include "gpio.h"
#include <iostream>
#include <fstream>
#include <sstream>
#include <stdexcept>
#include <string>
namespace GPIO {
class GPIOException : public std::runtime_error {
public:
explicit GPIOException(const std::string& message) : std::runtime_error(message) {}
};
void GPIOExport(int pin) {
std::ofstream exportFile("/sys/class/gpio/export");
if (!exportFile.is_open()) throw GPIOException("Failed to open export for writing!");
exportFile << pin;
}
void GPIOUnexport(int pin) {
std::ofstream unexportFile("/sys/class/gpio/unexport");
if (!unexportFile.is_open()) throw GPIOException("Failed to open unexport for writing!");
unexportFile << pin;
}
void GPIODirection(int pin, Direction dir) {
std::stringstream ss;
ss << "/sys/class/gpio/gpio" << pin << "/direction";
std::ofstream directionFile(ss.str());
if (!directionFile.is_open()) throw GPIOException("Failed to open gpio direction for writing!");
directionFile << (dir == Direction::IN ? "in" : "out");
}
int GPIORead(int pin) {
std::stringstream ss;
ss << "/sys/class/gpio/gpio" << pin << "/value";
std::ifstream valueFile(ss.str());
if (!valueFile.is_open()) throw GPIOException("Failed to open gpio value for reading!");
int value;
valueFile >> value;
return value;
}
void GPIOWrite(int pin, Value value) {
std::stringstream ss;
ss << "/sys/class/gpio/gpio" << pin << "/value";
std::ofstream valueFile(ss.str());
if (!valueFile.is_open()) throw GPIOException("Failed to open gpio value for writing!");
valueFile << (value == Value::LOW ? "0" : "1");
}
}
\ No newline at end of file
#pragma once
namespace GPIO {
enum class Direction { IN, OUT };
enum class Value { LOW, HIGH };
void GPIOExport(int pin);
void GPIOUnexport(int pin);
void GPIODirection(int pin, Direction dir);
int GPIORead(int pin);
void GPIOWrite(int pin, Value value);
}
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment