Skip to content

Commit b5e12b7

Browse files
committed
add:時間指定動画作成の追加
1 parent 632ee8c commit b5e12b7

File tree

6 files changed

+267
-0
lines changed

6 files changed

+267
-0
lines changed

Makefile

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ help:
2727
@echo " $$ make test"
2828
@echo 画像をサーバーにアップロードする
2929
@echo " $$ make upload-image"
30+
@echo ライントレース画像から動画を作成する
31+
@echo " $$ make create-line-trace-video"
32+
@echo " $$ make create-line-trace-video DURATION=60"
33+
@echo " $$ make create-line-trace-video START=60 END=100"
3034

3135
## 実行関連 ##
3236
build: build-client build-camera
@@ -41,6 +45,7 @@ build-camera:
4145
start: start-camera start-client
4246

4347
start-client:
48+
rm -rf camera_server/datafiles/line_trace
4449
cd $(MAKEFILE_PATH)../ && make start
4550

4651
start-camera:
@@ -155,3 +160,32 @@ upload-image:
155160
# ミニフィグの正面らしさ比較用画像をサーバーにアップロードする
156161
upload-minifig-image:
157162
curl --fail -X POST -F "file=@$(FILE_PATH)" http://$(SERVER_IP):8000/minifig/detect
163+
164+
## デバッグ関連 ##
165+
# ライントレース画像から動画を作成(ROI描画付き)
166+
# 使用例:
167+
# make create-line-trace-video # 全体(最大120秒)
168+
# make create-line-trace-video DURATION=60 # 最初の60秒
169+
# make create-line-trace-video START=60 END=100 # 60秒から100秒まで
170+
# make create-line-trace-video DURATION=0 START=60 END=100 # 60秒から100秒まで(DURATIONは無視される)
171+
create-line-trace-video:
172+
@if [ ! -d "$(MAKEFILE_PATH)camera_server/datafiles/line_trace" ]; then \
173+
echo "Error: camera_server/datafiles/line_trace not found"; \
174+
exit 1; \
175+
fi
176+
@echo "動画作成ツールをコンパイル中..."
177+
@cd $(MAKEFILE_PATH)camera_server && \
178+
g++ -std=c++17 -Wall -Wextra -O2 $$(pkg-config --cflags opencv4) \
179+
create_video.cpp $$(pkg-config --libs opencv4) -o create_video_app
180+
@echo "動画を作成中..."
181+
@cd $(MAKEFILE_PATH)camera_server && { \
182+
if [ -n "$(START)" ] && [ -n "$(END)" ]; then \
183+
./create_video_app datafiles/line_trace $(MAKEFILE_PATH)line_trace.mp4 $${DURATION:-0} $(START) $(END); \
184+
elif [ -n "$(DURATION)" ]; then \
185+
./create_video_app datafiles/line_trace $(MAKEFILE_PATH)line_trace.mp4 $(DURATION); \
186+
else \
187+
./create_video_app datafiles/line_trace $(MAKEFILE_PATH)line_trace.mp4; \
188+
fi; \
189+
}
190+
@rm -f $(MAKEFILE_PATH)camera_server/create_video_app
191+
@echo "動画を作成しました: $(MAKEFILE_PATH)line_trace.mp4"

camera_server/create_video.cpp

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
/**
2+
* @file create_video.cpp
3+
* @brief ライントレース画像から動画を作成するツール
4+
* @author Hara1274 HaruArima08
5+
*/
6+
7+
#include <opencv2/opencv.hpp>
8+
#include <iostream>
9+
#include <vector>
10+
#include <string>
11+
#include <filesystem>
12+
#include <algorithm>
13+
14+
int main(int argc, char* argv[])
15+
{
16+
if(argc < 3 || argc > 6) {
17+
std::cerr << "Usage: " << argv[0]
18+
<< " <input_dir> <output_video> [duration_seconds] [start_seconds] [end_seconds]"
19+
<< std::endl;
20+
std::cerr << "Examples:" << std::endl;
21+
std::cerr << " " << argv[0] << " input_dir output.mp4" << std::endl;
22+
std::cerr << " " << argv[0] << " input_dir output.mp4 120" << std::endl;
23+
std::cerr << " " << argv[0] << " input_dir output.mp4 0 60 100" << std::endl;
24+
return 1;
25+
}
26+
27+
std::string inputDir = argv[1];
28+
std::string outputPath = argv[2];
29+
int durationSeconds = (argc >= 4) ? std::stoi(argv[3]) : 120; // デフォルト120秒
30+
int startSeconds = (argc >= 5) ? std::stoi(argv[4]) : 0; // デフォルト0秒から
31+
int endSeconds = (argc == 6) ? std::stoi(argv[5]) : -1; // デフォルト指定なし
32+
33+
namespace fs = std::filesystem;
34+
35+
// 入力ディレクトリの存在確認
36+
if(!fs::exists(inputDir) || !fs::is_directory(inputDir)) {
37+
std::cerr << "Error: " << inputDir << " not found or is not a directory" << std::endl;
38+
return 1;
39+
}
40+
41+
// 画像ファイルを収集してソート
42+
std::vector<fs::path> imageFiles;
43+
for(const auto& entry : fs::directory_iterator(inputDir)) {
44+
if(entry.is_regular_file()) {
45+
std::string filename = entry.path().filename().string();
46+
if(filename.find("roi_") == 0 && filename.find(".JPEG") != std::string::npos) {
47+
imageFiles.push_back(entry.path());
48+
}
49+
}
50+
}
51+
52+
if(imageFiles.empty()) {
53+
std::cerr << "No image files found in " << inputDir << std::endl;
54+
return 1;
55+
}
56+
57+
// ファイル名でソート(タイムスタンプ順)
58+
std::sort(imageFiles.begin(), imageFiles.end(), [](const fs::path& a, const fs::path& b) {
59+
std::string nameA = a.filename().string();
60+
std::string nameB = b.filename().string();
61+
62+
// タイムスタンプを抽出(最後の_以降の数字)
63+
size_t posA = nameA.rfind('_');
64+
size_t posB = nameB.rfind('_');
65+
66+
if(posA != std::string::npos && posB != std::string::npos) {
67+
long long tsA = std::stoll(nameA.substr(posA + 1));
68+
long long tsB = std::stoll(nameB.substr(posB + 1));
69+
return tsA < tsB;
70+
}
71+
return nameA < nameB;
72+
});
73+
74+
std::cerr << "Found " << imageFiles.size() << " images" << std::endl;
75+
76+
// 最初の画像を読み込んでサイズを取得
77+
cv::Mat firstFrame = cv::imread(imageFiles[0].string());
78+
if(firstFrame.empty()) {
79+
std::cerr << "Failed to read first image: " << imageFiles[0] << std::endl;
80+
return 1;
81+
}
82+
83+
// 解像度を縮小
84+
double scale = 0.5;
85+
cv::Size frameSize(static_cast<int>(firstFrame.cols * scale),
86+
static_cast<int>(firstFrame.rows * scale));
87+
std::cerr << "Original size: " << firstFrame.cols << "x" << firstFrame.rows << std::endl;
88+
std::cerr << "Video size: " << frameSize.width << "x" << frameSize.height << std::endl;
89+
90+
// VideoWriterを作成
91+
double fps = 15.0;
92+
cv::VideoWriter writer(outputPath, cv::VideoWriter::fourcc('H', '2', '6', '4'), fps, frameSize);
93+
if(!writer.isOpened()) {
94+
std::cerr << "Failed to open VideoWriter: " << outputPath << std::endl;
95+
return 1;
96+
}
97+
98+
// 時間指定の処理
99+
double totalDuration = static_cast<double>(imageFiles.size()) / fps;
100+
101+
// 終了時間が指定されていない場合は、開始時間+durationSecondsまたは全体の長さ
102+
if(endSeconds == -1) {
103+
if(startSeconds > 0) {
104+
endSeconds = startSeconds + durationSeconds;
105+
} else {
106+
endSeconds = std::min(durationSeconds, 120);
107+
}
108+
}
109+
110+
// 範囲チェック
111+
if(startSeconds < 0) startSeconds = 0;
112+
if(endSeconds > totalDuration) endSeconds = static_cast<int>(totalDuration);
113+
if(startSeconds >= endSeconds) {
114+
std::cerr << "Error: start_seconds (" << startSeconds << ") must be less than end_seconds ("
115+
<< endSeconds << ")" << std::endl;
116+
return 1;
117+
}
118+
119+
// フレーム範囲の計算
120+
int startFrame = static_cast<int>(fps * startSeconds);
121+
int endFrame = static_cast<int>(fps * endSeconds);
122+
123+
std::cerr << "Total images: " << imageFiles.size() << " (" << totalDuration << " seconds at "
124+
<< fps << " fps)" << std::endl;
125+
std::cerr << "Creating video from " << startSeconds << "s to " << endSeconds << "s (frames "
126+
<< startFrame << "-" << endFrame << ")" << std::endl;
127+
128+
// 各画像を処理
129+
int frameCount = 0;
130+
for(size_t i = 0; i < imageFiles.size(); i++) {
131+
// 指定された時間範囲外のフレームをスキップ
132+
if(static_cast<int>(i) < startFrame) continue;
133+
if(static_cast<int>(i) >= endFrame) break;
134+
135+
const auto& imagePath = imageFiles[i];
136+
137+
cv::Mat frame = cv::imread(imagePath.string());
138+
if(frame.empty()) {
139+
std::cerr << "Failed to read image: " << imagePath << std::endl;
140+
continue;
141+
}
142+
143+
// --- ここから変更点:リサイズを先に行い、ROIはスケール後の座標で描画 ---
144+
cv::Mat resizedFrame;
145+
cv::resize(frame, resizedFrame, frameSize, 0, 0, cv::INTER_AREA);
146+
147+
// ファイル名からROI情報を抽出
148+
std::string filename = imagePath.filename().string();
149+
int x = 0, y = 0, w = 0, h = 0;
150+
151+
size_t xPos = filename.find("_x");
152+
size_t yPos = filename.find("_y");
153+
size_t wPos = filename.find("_w");
154+
size_t hPos = filename.find("_h");
155+
156+
if(xPos != std::string::npos && yPos != std::string::npos && wPos != std::string::npos
157+
&& hPos != std::string::npos) {
158+
try {
159+
x = std::stoi(filename.substr(xPos + 2, yPos - xPos - 2));
160+
y = std::stoi(filename.substr(yPos + 2, wPos - yPos - 2));
161+
w = std::stoi(filename.substr(wPos + 2, hPos - wPos - 2));
162+
h = std::stoi(filename.substr(hPos + 2));
163+
} catch(const std::exception& e) {
164+
std::cerr << "Failed to parse ROI from filename: " << filename << std::endl;
165+
// リサイズ済みフレームを書き出して次へ
166+
writer.write(resizedFrame);
167+
frameCount++;
168+
continue;
169+
}
170+
171+
// スケールに合わせたROI座標
172+
int sx = static_cast<int>(x * scale);
173+
int sy = static_cast<int>(y * scale);
174+
int sw = static_cast<int>(w * scale);
175+
int sh = static_cast<int>(h * scale);
176+
177+
// 範囲チェック(簡潔に調整)
178+
if(sx < 0) sx = 0;
179+
if(sy < 0) sy = 0;
180+
if(sw < 0) sw = 0;
181+
if(sh < 0) sh = 0;
182+
if(sx + sw > resizedFrame.cols) sw = resizedFrame.cols - sx;
183+
if(sy + sh > resizedFrame.rows) sh = resizedFrame.rows - sy;
184+
185+
// ROI矩形を描画(赤色、太さ2)
186+
if(sw > 0 && sh > 0) {
187+
cv::rectangle(resizedFrame, cv::Rect(sx, sy, sw, sh), cv::Scalar(0, 0, 255), 2);
188+
}
189+
190+
// ROI文字情報を描画
191+
std::string roiText1 = "ROI: x=" + std::to_string(x) + ", y=" + std::to_string(y);
192+
std::string roiText2 = "w=" + std::to_string(w) + ", h=" + std::to_string(h);
193+
cv::putText(resizedFrame, roiText1, cv::Point(10, 22), cv::FONT_HERSHEY_SIMPLEX, 0.6,
194+
cv::Scalar(0, 0, 255), 1);
195+
cv::putText(resizedFrame, roiText2, cv::Point(10, 22 + 18), cv::FONT_HERSHEY_SIMPLEX, 0.6,
196+
cv::Scalar(0, 0, 255), 1);
197+
}
198+
199+
// 動画に書き込み
200+
writer.write(resizedFrame);
201+
frameCount++;
202+
}
203+
204+
writer.release();
205+
206+
// 元の画像ディレクトリを削除
207+
try {
208+
fs::remove_all(inputDir);
209+
std::cerr << "Removed directory: " << inputDir << std::endl;
210+
} catch(const std::exception& e) {
211+
std::cerr << "Failed to remove directory: " << e.what() << std::endl;
212+
}
213+
214+
std::cerr << "動画を作成しました: " << outputPath << std::endl;
215+
216+
return 0;
217+
}

camera_server/modules/image_processors/LineBoundingBoxDetector.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,13 @@ void LineBoundingBoxDetector::detect(const cv::Mat& frame, BoundingBoxDetectionR
6060
return;
6161
}
6262

63+
// 画像保存
64+
std::string filename
65+
= "roi_x" + std::to_string(roi.x) + "_y" + std::to_string(roi.y) + "_w"
66+
+ std::to_string(roi.width) + "_h" + std::to_string(roi.height) + "_"
67+
+ std::to_string(std::chrono::system_clock::now().time_since_epoch().count());
68+
FrameSave::save(const_cast<cv::Mat&>(frame), "datafiles/line_trace", filename);
69+
6370
// 入力画像が指定の解像度と異なる場合、リサイズ処理
6471
cv::Mat frameProcessed;
6572
if(frame.size() != resolution) {

camera_server/modules/image_processors/LineBoundingBoxDetector.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
#include "BoundingBoxDetector.h"
1111
#include "SystemInfo.h"
12+
#include "FrameSave.h"
1213

1314
class LineBoundingBoxDetector : public BoundingBoxDetector {
1415
public:

camera_server/modules/image_processors/TwoColorLineBoundingBoxDetector.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,13 @@ void TwoColorLineBoundingBoxDetector::detect(const cv::Mat& frame,
7171
return;
7272
}
7373

74+
// 画像保存
75+
std::string filename
76+
= "roi_x" + std::to_string(roi.x) + "_y" + std::to_string(roi.y) + "_w"
77+
+ std::to_string(roi.width) + "_h" + std::to_string(roi.height) + "_"
78+
+ std::to_string(std::chrono::system_clock::now().time_since_epoch().count());
79+
FrameSave::save(const_cast<cv::Mat&>(frame), "datafiles/line_trace", filename);
80+
7481
// 1. リサイズ処理
7582
cv::Mat frameProcessed;
7683
if(frame.size() != resolution) {

camera_server/modules/image_processors/TwoColorLineBoundingBoxDetector.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
#include "BoundingBoxDetector.h"
1111
#include "SystemInfo.h"
12+
#include "FrameSave.h"
1213

1314
class TwoColorLineBoundingBoxDetector {
1415
public:

0 commit comments

Comments
 (0)