/src/ffmpegthumbs/ffmpegthumbnailer/videothumbnailer.cpp
Line | Count | Source |
1 | | /* |
2 | | SPDX-FileCopyrightText: 2010 Dirk Vanden Boer <dirk.vdb@gmail.com> |
3 | | |
4 | | SPDX-License-Identifier: GPL-2.0-or-later |
5 | | */ |
6 | | |
7 | | #include "videothumbnailer.h" |
8 | | |
9 | | #include "moviedecoder.h" |
10 | | #include "filmstripfilter.h" |
11 | | #include "imagewriter.h" |
12 | | |
13 | | #include <QtGlobal> |
14 | | #include <QTime> |
15 | | |
16 | | #include <iostream> |
17 | | #include <cfloat> |
18 | | #include <cmath> |
19 | | #include <sys/stat.h> |
20 | | |
21 | | |
22 | | using namespace std; |
23 | | |
24 | | namespace ffmpegthumbnailer |
25 | | { |
26 | | |
27 | | static const int SMART_FRAME_ATTEMPTS = 25; |
28 | | |
29 | | VideoThumbnailer::VideoThumbnailer() |
30 | 682 | : m_ThumbnailSize(128) |
31 | 682 | , m_SeekPercentage(10) |
32 | 682 | , m_OverlayFilmStrip(false) |
33 | 682 | , m_WorkAroundIssues(false) |
34 | 682 | , m_MaintainAspectRatio(true) |
35 | 682 | , m_SmartFrameSelection(false) |
36 | 682 | { |
37 | 682 | } |
38 | | |
39 | | VideoThumbnailer::VideoThumbnailer(int thumbnailSize, bool workaroundIssues, bool maintainAspectRatio, bool smartFrameSelection) |
40 | 0 | : m_ThumbnailSize(thumbnailSize) |
41 | 0 | , m_SeekPercentage(10) |
42 | 0 | , m_WorkAroundIssues(workaroundIssues) |
43 | 0 | , m_MaintainAspectRatio(maintainAspectRatio) |
44 | 0 | , m_SmartFrameSelection(smartFrameSelection) |
45 | 0 | { |
46 | 0 | } |
47 | | |
48 | | VideoThumbnailer::~VideoThumbnailer() |
49 | 682 | { |
50 | 682 | } |
51 | | |
52 | | void VideoThumbnailer::setSeekPercentage(int percentage) |
53 | 682 | { |
54 | 682 | m_SeekTime.clear(); |
55 | 682 | m_SeekPercentage = percentage > 95 ? 95 : percentage; |
56 | 682 | } |
57 | | |
58 | | void VideoThumbnailer::setSeekTime(const QString& seekTime) |
59 | 0 | { |
60 | 0 | m_SeekTime = seekTime; |
61 | 0 | } |
62 | | |
63 | | void VideoThumbnailer::setThumbnailSize(int size) |
64 | 682 | { |
65 | 682 | m_ThumbnailSize = size; |
66 | 682 | } |
67 | | |
68 | | void VideoThumbnailer::setWorkAroundIssues(bool workAround) |
69 | 0 | { |
70 | 0 | m_WorkAroundIssues = workAround; |
71 | 0 | } |
72 | | |
73 | | void VideoThumbnailer::setMaintainAspectRatio(bool enabled) |
74 | 0 | { |
75 | 0 | m_MaintainAspectRatio = enabled; |
76 | 0 | } |
77 | | |
78 | | void VideoThumbnailer::setSmartFrameSelection(bool enabled) |
79 | 0 | { |
80 | 0 | m_SmartFrameSelection = enabled; |
81 | 0 | } |
82 | | |
83 | | int timeToSeconds(const QString& time) |
84 | 0 | { |
85 | 0 | return QTime::fromString(time, QLatin1String("hh:mm:ss")).secsTo(QTime(0, 0, 0)); |
86 | 0 | } |
87 | | |
88 | | void VideoThumbnailer::generateThumbnail(const QString& videoFile, ImageWriter& imageWriter, QImage &image) |
89 | 682 | { |
90 | 682 | MovieDecoder movieDecoder(videoFile, nullptr); |
91 | 682 | if (movieDecoder.getInitialized()) { |
92 | 0 | if (!movieDecoder.decodeVideoFrame()) { //before seeking, a frame has to be decoded |
93 | 0 | return; |
94 | 0 | } |
95 | | |
96 | 0 | if ((!m_WorkAroundIssues) || (movieDecoder.getCodec() != QLatin1String("h264"))) { //workaround for bug in older ffmpeg (100% cpu usage when seeking in h264 files) |
97 | 0 | int secondToSeekTo = m_SeekTime.isEmpty() ? movieDecoder.getDuration() * m_SeekPercentage / 100 : timeToSeconds(m_SeekTime); |
98 | 0 | movieDecoder.seek(secondToSeekTo); |
99 | 0 | } |
100 | | |
101 | 0 | VideoFrame videoFrame; |
102 | | |
103 | 0 | if (m_SmartFrameSelection) { |
104 | 0 | generateSmartThumbnail(movieDecoder, videoFrame); |
105 | 0 | } else { |
106 | 0 | movieDecoder.getScaledVideoFrame(m_ThumbnailSize, m_MaintainAspectRatio, videoFrame); |
107 | 0 | } |
108 | | |
109 | 0 | applyFilters(videoFrame); |
110 | 0 | imageWriter.writeFrame(videoFrame, image, movieDecoder.transformations()); |
111 | 0 | } |
112 | 682 | } |
113 | | |
114 | | void VideoThumbnailer::generateSmartThumbnail(MovieDecoder& movieDecoder, VideoFrame& videoFrame) |
115 | 0 | { |
116 | 0 | vector<VideoFrame> videoFrames(SMART_FRAME_ATTEMPTS); |
117 | 0 | vector<Histogram<int> > histograms(SMART_FRAME_ATTEMPTS); |
118 | |
|
119 | 0 | for (int i = 0; i < SMART_FRAME_ATTEMPTS; ++i) { |
120 | 0 | movieDecoder.decodeVideoFrame(); |
121 | 0 | movieDecoder.getScaledVideoFrame(m_ThumbnailSize, m_MaintainAspectRatio, videoFrames[i]); |
122 | 0 | generateHistogram(videoFrames[i], histograms[i]); |
123 | 0 | } |
124 | |
|
125 | 0 | int bestFrame = getBestThumbnailIndex(videoFrames, histograms); |
126 | |
|
127 | 0 | Q_ASSERT(bestFrame != -1); |
128 | 0 | videoFrame = videoFrames[bestFrame]; |
129 | 0 | } |
130 | | |
131 | | void VideoThumbnailer::generateThumbnail(const QString& videoFile, QImage &image) |
132 | 682 | { |
133 | 682 | ImageWriter* imageWriter = new ImageWriter(); |
134 | 682 | generateThumbnail(videoFile, *imageWriter, image); |
135 | 682 | delete imageWriter; |
136 | 682 | } |
137 | | |
138 | | void VideoThumbnailer::addFilter(IFilter* filter) |
139 | 682 | { |
140 | 682 | m_Filters.push_back(filter); |
141 | 682 | } |
142 | | |
143 | | void VideoThumbnailer::removeFilter(IFilter* filter) |
144 | 0 | { |
145 | 0 | for (vector<IFilter*>::iterator iter = m_Filters.begin(); |
146 | 0 | iter != m_Filters.end(); |
147 | 0 | ++iter) { |
148 | 0 | if (*iter == filter) { |
149 | 0 | m_Filters.erase(iter); |
150 | 0 | break; |
151 | 0 | } |
152 | 0 | } |
153 | 0 | } |
154 | | |
155 | | void VideoThumbnailer::clearFilters() |
156 | 0 | { |
157 | 0 | m_Filters.clear(); |
158 | 0 | } |
159 | | |
160 | | void VideoThumbnailer::applyFilters(VideoFrame& videoFrame) |
161 | 0 | { |
162 | 0 | for (vector<IFilter*>::iterator iter = m_Filters.begin(); |
163 | 0 | iter != m_Filters.end(); |
164 | 0 | ++iter) { |
165 | 0 | (*iter)->process(videoFrame); |
166 | 0 | } |
167 | 0 | } |
168 | | |
169 | | void VideoThumbnailer::generateHistogram(const VideoFrame& videoFrame, Histogram<int>& histogram) |
170 | 0 | { |
171 | 0 | for (quint32 i = 0; i < videoFrame.height; ++i) { |
172 | 0 | int pixelIndex = i * videoFrame.lineSize; |
173 | 0 | for (quint32 j = 0; j < videoFrame.width * 3; j += 3) { |
174 | 0 | ++histogram.r[videoFrame.frameData[pixelIndex + j]]; |
175 | 0 | ++histogram.g[videoFrame.frameData[pixelIndex + j + 1]]; |
176 | 0 | ++histogram.b[videoFrame.frameData[pixelIndex + j + 2]]; |
177 | 0 | } |
178 | 0 | } |
179 | 0 | } |
180 | | |
181 | | int VideoThumbnailer::getBestThumbnailIndex(vector<VideoFrame>& videoFrames, const vector<Histogram<int> >& histograms) |
182 | 0 | { |
183 | 0 | Q_UNUSED(videoFrames); |
184 | 0 | Histogram<float> avgHistogram; |
185 | 0 | for (size_t i = 0; i < histograms.size(); ++i) { |
186 | 0 | for (int j = 0; j < 255; ++j) { |
187 | 0 | avgHistogram.r[j] += static_cast<float>(histograms[i].r[j]) / histograms.size(); |
188 | 0 | avgHistogram.g[j] += static_cast<float>(histograms[i].g[j]) / histograms.size(); |
189 | 0 | avgHistogram.b[j] += static_cast<float>(histograms[i].b[j]) / histograms.size(); |
190 | 0 | } |
191 | 0 | } |
192 | |
|
193 | 0 | int bestFrame = -1; |
194 | 0 | float minRMSE = FLT_MAX; |
195 | 0 | for (size_t i = 0; i < histograms.size(); ++i) { |
196 | | //calculate root mean squared error |
197 | 0 | float rmse = 0.0; |
198 | 0 | for (int j = 0; j < 255; ++j) { |
199 | 0 | float error = fabsf(avgHistogram.r[j] - histograms[i].r[j]) |
200 | 0 | + fabsf(avgHistogram.g[j] - histograms[i].g[j]) |
201 | 0 | + fabsf(avgHistogram.b[j] - histograms[i].b[j]); |
202 | 0 | rmse += (error * error) / 255; |
203 | 0 | } |
204 | |
|
205 | 0 | rmse = sqrtf(rmse); |
206 | 0 | if (rmse < minRMSE) { |
207 | 0 | minRMSE = rmse; |
208 | 0 | bestFrame = i; |
209 | 0 | } |
210 | 0 | } |
211 | | #ifdef DEBUG_MODE |
212 | | cout << "Best frame was: " << bestFrame << "(RMSE: " << minRMSE << ")" << endl; |
213 | | #endif |
214 | 0 | return bestFrame; |
215 | 0 | } |
216 | | |
217 | | } |