File indexing completed on 2025-04-27 03:58:10
0001 /* ============================================================ 0002 * 0003 * This file is a part of digiKam project 0004 * https://www.digikam.org 0005 * 0006 * Date : 2016-04-21 0007 * Description : video thumbnails extraction based on ffmpeg 0008 * 0009 * SPDX-FileCopyrightText: 2010 by Dirk Vanden Boer <dirk dot vdb at gmail dot com> 0010 * SPDX-FileCopyrightText: 2016-2018 by Maik Qualmann <metzpinguin at gmail dot com> 0011 * SPDX-FileCopyrightText: 2016-2024 by Gilles Caulier <caulier dot gilles at gmail dot com> 0012 * 0013 * SPDX-License-Identifier: GPL-2.0-or-later 0014 * 0015 * ============================================================ */ 0016 0017 #include "videothumbnailer.h" 0018 0019 // C++ includes 0020 0021 #include <cfloat> 0022 0023 // Qt includes 0024 0025 #include <QtGlobal> 0026 #include <QtMath> 0027 #include <QTime> 0028 0029 // Local includes 0030 0031 #include "videothumbdecoder.h" 0032 #include "videostripfilter.h" 0033 #include "videothumbwriter.h" 0034 #include "digikam_debug.h" 0035 0036 using namespace std; 0037 0038 namespace Digikam 0039 { 0040 0041 class Q_DECL_HIDDEN VideoThumbnailer::Private 0042 { 0043 public: 0044 0045 template <typename T> 0046 class Q_DECL_HIDDEN Histogram 0047 { 0048 0049 public: 0050 0051 explicit Histogram() 0052 { 0053 memset(r, 0, 255 * sizeof(T)); 0054 memset(g, 0, 255 * sizeof(T)); 0055 memset(b, 0, 255 * sizeof(T)); 0056 } 0057 0058 ~Histogram() 0059 { 0060 } 0061 0062 public: 0063 0064 T r[256]; 0065 T g[256]; 0066 T b[256]; 0067 }; 0068 0069 public: 0070 0071 explicit Private() 0072 : thumbnailSize (256), 0073 seekPercentage (10), 0074 overlayFilmStrip (false), 0075 workAroundIssues (false), 0076 maintainAspectRatio (true), 0077 smartFrameSelection (false), 0078 SMART_FRAME_ATTEMPTS(25) 0079 { 0080 } 0081 0082 void generateHistogram(const VideoFrame& videoFrame, Histogram<int>& histogram); 0083 int getBestThumbnailIndex(std::vector<VideoFrame>& videoFrames, 0084 const std::vector<Histogram<int> >& histograms); 0085 0086 public: 0087 0088 int thumbnailSize; 0089 quint16 seekPercentage; 0090 bool overlayFilmStrip; 0091 bool workAroundIssues; 0092 bool maintainAspectRatio; 0093 bool smartFrameSelection; 0094 QString seekTime; 0095 QVector<VideoStripFilter*> filters; 0096 0097 const int SMART_FRAME_ATTEMPTS; 0098 }; 0099 0100 VideoThumbnailer::VideoThumbnailer() 0101 : d(new Private) 0102 { 0103 } 0104 0105 VideoThumbnailer::VideoThumbnailer(int thumbnailSize, 0106 bool workaroundIssues, 0107 bool maintainAspectRatio, 0108 bool smartFrameSelection) 0109 : d(new Private) 0110 { 0111 d->thumbnailSize = thumbnailSize; 0112 d->workAroundIssues = workaroundIssues; 0113 d->maintainAspectRatio = maintainAspectRatio; 0114 d->smartFrameSelection = smartFrameSelection; 0115 } 0116 0117 VideoThumbnailer::~VideoThumbnailer() 0118 { 0119 delete d; 0120 } 0121 0122 void VideoThumbnailer::setSeekPercentage(int percentage) 0123 { 0124 d->seekTime.clear(); 0125 d->seekPercentage = (percentage > 95) ? 95 : percentage; 0126 } 0127 0128 void VideoThumbnailer::setSeekTime(const QString& seekTime) 0129 { 0130 d->seekTime = seekTime; 0131 } 0132 0133 void VideoThumbnailer::setThumbnailSize(int size) 0134 { 0135 d->thumbnailSize = size; 0136 } 0137 0138 void VideoThumbnailer::setWorkAroundIssues(bool workAround) 0139 { 0140 d->workAroundIssues = workAround; 0141 } 0142 0143 void VideoThumbnailer::setMaintainAspectRatio(bool enabled) 0144 { 0145 d->maintainAspectRatio = enabled; 0146 } 0147 0148 void VideoThumbnailer::setSmartFrameSelection(bool enabled) 0149 { 0150 d->smartFrameSelection = enabled; 0151 } 0152 0153 int VideoThumbnailer::timeToSeconds(const QString& time) const 0154 { 0155 return QTime::fromString(time, QLatin1String("hh:mm:ss")).secsTo(QTime(0, 0, 0)); 0156 } 0157 0158 void VideoThumbnailer::generateThumbnail(const QString& videoFile, 0159 VideoThumbWriter& imageWriter, 0160 QImage &image) 0161 { 0162 VideoThumbDecoder movieDecoder(videoFile); 0163 0164 if (movieDecoder.getInitialized()) 0165 { 0166 // before seeking, a frame has to be decoded 0167 0168 if (!movieDecoder.decodeVideoFrame()) 0169 { 0170 return; 0171 } 0172 0173 if ((!d->workAroundIssues) || (movieDecoder.getCodec() != QLatin1String("h264"))) 0174 { 0175 // workaround for bug in older ffmpeg (100% cpu usage when seeking in h264 files) 0176 0177 int secondToSeekTo = d->seekTime.isEmpty() ? movieDecoder.getDuration() * d->seekPercentage / 100 0178 : timeToSeconds(d->seekTime); 0179 movieDecoder.seek(secondToSeekTo); 0180 } 0181 0182 VideoFrame videoFrame; 0183 0184 if (d->smartFrameSelection) 0185 { 0186 generateSmartThumbnail(movieDecoder, videoFrame); 0187 } 0188 else 0189 { 0190 movieDecoder.getScaledVideoFrame(d->thumbnailSize, d->maintainAspectRatio, videoFrame); 0191 } 0192 0193 applyFilters(videoFrame); 0194 imageWriter.writeFrame(videoFrame, image); 0195 } 0196 } 0197 0198 void VideoThumbnailer::generateSmartThumbnail(VideoThumbDecoder& movieDecoder, 0199 VideoFrame& videoFrame) 0200 { 0201 vector<VideoFrame> videoFrames(d->SMART_FRAME_ATTEMPTS); 0202 vector<Private::Histogram<int> > histograms(d->SMART_FRAME_ATTEMPTS); 0203 0204 for (int i = 0 ; i < d->SMART_FRAME_ATTEMPTS ; ++i) 0205 { 0206 movieDecoder.decodeVideoFrame(); 0207 movieDecoder.getScaledVideoFrame(d->thumbnailSize, d->maintainAspectRatio, videoFrames[i]); 0208 d->generateHistogram(videoFrames[i], histograms[i]); 0209 } 0210 0211 int bestFrame = d->getBestThumbnailIndex(videoFrames, histograms); 0212 0213 if (bestFrame == -1) 0214 { 0215 bestFrame = 0; 0216 } 0217 0218 videoFrame = videoFrames[bestFrame]; 0219 } 0220 0221 void VideoThumbnailer::generateThumbnail(const QString& videoFile, 0222 QImage &image) 0223 { 0224 VideoThumbWriter* const imageWriter = new VideoThumbWriter(); 0225 generateThumbnail(videoFile, *imageWriter, image); 0226 delete imageWriter; 0227 } 0228 0229 void VideoThumbnailer::addFilter(VideoStripFilter* const filter) 0230 { 0231 d->filters.append(filter); 0232 } 0233 0234 void VideoThumbnailer::removeFilter(const VideoStripFilter* const filter) 0235 { 0236 for (QVector<VideoStripFilter*>::iterator it = d->filters.begin() ; 0237 it != d->filters.end() ; ++it) 0238 { 0239 if (*it == filter) 0240 { 0241 d->filters.erase(it); 0242 break; 0243 } 0244 } 0245 } 0246 0247 void VideoThumbnailer::clearFilters() 0248 { 0249 d->filters.clear(); 0250 } 0251 0252 void VideoThumbnailer::applyFilters(VideoFrame& videoFrame) 0253 { 0254 for (QVector<VideoStripFilter*>::iterator it = d->filters.begin() ; 0255 it != d->filters.end() ; ++it) 0256 { 0257 (*it)->process(videoFrame); 0258 } 0259 } 0260 0261 void VideoThumbnailer::Private::generateHistogram(const VideoFrame& videoFrame, 0262 Private::Histogram<int>& histogram) 0263 { 0264 for (quint32 i = 0 ; i < videoFrame.height ; ++i) 0265 { 0266 int pixelIndex = i * videoFrame.lineSize; 0267 0268 for (quint32 j = 0 ; j < videoFrame.width * 3 ; j += 3) 0269 { 0270 ++histogram.r[videoFrame.frameData[pixelIndex + j ]]; 0271 ++histogram.g[videoFrame.frameData[pixelIndex + j + 1]]; 0272 ++histogram.b[videoFrame.frameData[pixelIndex + j + 2]]; 0273 } 0274 } 0275 } 0276 0277 int VideoThumbnailer::Private::getBestThumbnailIndex(vector<VideoFrame>& videoFrames, 0278 const vector<Private::Histogram<int> >& histograms) 0279 { 0280 Q_UNUSED(videoFrames); 0281 Private::Histogram<float> avgHistogram; 0282 0283 for (size_t i = 0 ; i < histograms.size() ; ++i) 0284 { 0285 for (int j = 0 ; j < 255 ; ++j) 0286 { 0287 avgHistogram.r[j] += static_cast<float>(histograms[i].r[j]) / histograms.size(); 0288 avgHistogram.g[j] += static_cast<float>(histograms[i].g[j]) / histograms.size(); 0289 avgHistogram.b[j] += static_cast<float>(histograms[i].b[j]) / histograms.size(); 0290 } 0291 } 0292 0293 int bestFrame = -1; 0294 float minRMSE = FLT_MAX; 0295 0296 for (size_t i = 0 ; i < histograms.size() ; ++i) 0297 { 0298 // calculate root mean squared error 0299 0300 float rmse = 0.0; 0301 0302 for (int j = 0 ; j < 255 ; ++j) 0303 { 0304 float error = qFabs(avgHistogram.r[j] - histograms[i].r[j]) + 0305 qFabs(avgHistogram.g[j] - histograms[i].g[j]) + 0306 qFabs(avgHistogram.b[j] - histograms[i].b[j]); 0307 rmse += (error * error) / 255; 0308 } 0309 0310 rmse = qSqrt(rmse); 0311 0312 if (rmse < minRMSE) 0313 { 0314 minRMSE = rmse; 0315 bestFrame = i; 0316 } 0317 } 0318 /* 0319 qCDebug(DIGIKAM_GENERAL_LOG) << "Best frame was: " 0320 << bestFrame 0321 << "(RMSE: " 0322 << minRMSE 0323 << ")" << QT_ENDL; 0324 */ 0325 return bestFrame; 0326 } 0327 0328 } // namespace Digikam