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+ }
0 commit comments