diff --git a/tools/ld-diffdod/diffdod.cpp b/tools/ld-diffdod/diffdod.cpp index 7688f0622..09dcbf78c 100644 --- a/tools/ld-diffdod/diffdod.cpp +++ b/tools/ld-diffdod/diffdod.cpp @@ -23,68 +23,381 @@ ************************************************************************/ #include "diffdod.h" +#include "sources.h" -Diffdod::Diffdod(QObject *parent) : QObject(parent) +DiffDod::DiffDod(QAtomicInt& abort, Sources& sources, QObject *parent) + : QThread(parent), m_abort(abort), m_sources(sources) { } -bool Diffdod::process(QVector inputFilenames, bool reverse, - qint32 dodThreshold, bool lumaClip, - qint32 startVbi, qint32 lengthVbi) +// Run method for thread +void DiffDod::run() { - // Show input filenames - qInfo() << "Processing" << inputFilenames.size() << "input TBC files:"; - for (qint32 i = 0; i < inputFilenames.size(); i++) qInfo().nospace() << " Source #" << i << ": " << inputFilenames[i]; - - // And then show the rest... - if (reverse) qInfo() << "Using reverse field order"; else qInfo() << "Using normal field order"; - qInfo() << "Dropout detection threshold is" << dodThreshold; - if (lumaClip) qInfo() << "Performing luma clip detection"; else qInfo() << "Not performing luma clip detection"; - qInfo() << ""; - - // Load the input TBC files - if (!loadInputTbcFiles(inputFilenames, reverse)) { - qCritical() << "Error: Unable to load input TBC files - cannot continue!"; - return false; + // Set up the input variables + qint32 targetVbiFrame; + QVector firstFields; + QVector secondFields; + LdDecodeMetaData::VideoParameters videoParameters; + QVector availableSourcesForFrame; + qint32 dodThreshold; + bool signalClip; + + // Set up the output variables + QVector firstFieldDropouts; + QVector secondFieldDropouts; + + // Process frames until there's nothing left to process + while(!m_abort) { + // Get the next frame to process ------------------------------------------------------------------------------ + if (!m_sources.getInputFrame(targetVbiFrame, firstFields, secondFields, videoParameters, + availableSourcesForFrame, dodThreshold, signalClip)) { + // No more input fields --> exit + break; + } + + // Process the frame ------------------------------------------------------------------------------------------ + + // Create the field difference maps + // Make a vector to store the result of the diff + QVector firstFieldDiff; + QVector secondFieldDiff; + firstFieldDiff.resize(firstFields.size()); + secondFieldDiff.resize(secondFields.size()); + + // Resize the fieldDiff sub-vectors and default the elements to zero + for (qint32 sourcePointer = 0; sourcePointer < firstFieldDiff.size(); sourcePointer++) { + firstFieldDiff[sourcePointer].fill(0, videoParameters.fieldHeight * videoParameters.fieldWidth); + } + for (qint32 sourcePointer = 0; sourcePointer < secondFieldDiff.size(); sourcePointer++) { + secondFieldDiff[sourcePointer].fill(0, videoParameters.fieldHeight * videoParameters.fieldWidth); + } + + // Perform the clip check + if (signalClip) { + performClipCheck(firstFields, firstFieldDiff, videoParameters, availableSourcesForFrame); + performClipCheck(secondFields, secondFieldDiff, videoParameters, availableSourcesForFrame); + } + + // Filter the frame to leave just the luma information + performLumaFilter(firstFields, videoParameters, availableSourcesForFrame); + performLumaFilter(secondFields, videoParameters, availableSourcesForFrame); + + // Create a differential map of the fields for the avaialble frames (based on the DOD threshold) + getFieldErrorByMedian(firstFields, firstFieldDiff, dodThreshold, videoParameters, availableSourcesForFrame); + getFieldErrorByMedian(secondFields, secondFieldDiff, dodThreshold, videoParameters, availableSourcesForFrame); + + // Create the drop-out metadata based on the differential map of the fields + firstFieldDropouts = getFieldDropouts(firstFieldDiff, videoParameters, availableSourcesForFrame); + secondFieldDropouts = getFieldDropouts(secondFieldDiff, videoParameters, availableSourcesForFrame); + + // Concatenate dropouts on the same line that are close together (to cut down on the + // amount of generated metadata with noisy/bad sources) + concatenateFieldDropouts(firstFieldDropouts, availableSourcesForFrame); + concatenateFieldDropouts(secondFieldDropouts, availableSourcesForFrame); + + // Return the processed frame --------------------------------------------------------------------------------- + m_sources.setOutputFrame(targetVbiFrame, firstFieldDropouts, secondFieldDropouts, availableSourcesForFrame); + } +} + +// Private methods ---------------------------------------------------------------------------------------------------- + +// Create an error maps of the field based on absolute clipping of the input field values (i.e. +// where the signal clips on 0 or 65535 before any filtering) +void DiffDod::performClipCheck(QVector &fields, QVector &fieldDiff, + LdDecodeMetaData::VideoParameters videoParameters, + QVector availableSourcesForFrame) +{ + // Process the fields one line at a time + for (qint32 y = videoParameters.firstActiveFieldLine; y < videoParameters.lastActiveFieldLine; y++) { + qint32 startOfLinePointer = y * videoParameters.fieldWidth; + + for (qint32 sourcePointer = 0; sourcePointer < availableSourcesForFrame.size(); sourcePointer++) { + qint32 sourceNo = availableSourcesForFrame[sourcePointer]; // Get the actual source + + for (qint32 x = videoParameters.colourBurstStart; x < videoParameters.activeVideoEnd; x++) { + // Get the IRE value for the source field, cast to 32 bit signed + qint32 sourceIre = static_cast(fields[sourceNo][x + startOfLinePointer]); + + // Check for a luma clip event + if ((sourceIre == 0) || (sourceIre == 65535)) { + // Signal has clipped, scan back and forth looking for the start + // and end points of the event (i.e. the point where the event + // goes back into the expected range) + qint32 range = 10; // maximum + and - scan range + qint32 minX = x - range; + if (minX < videoParameters.activeVideoStart) minX = videoParameters.activeVideoStart; + qint32 maxX = x + range; + if (maxX > videoParameters.activeVideoEnd) maxX = videoParameters.activeVideoEnd; + + qint32 startX = x; + qint32 endX = x; + + for (qint32 i = x; i > minX; i--) { + qint32 ire = static_cast(fields[sourceNo][x + startOfLinePointer]); + if (ire > 200 || ire < 65335) { + startX = i; + } + } + + for (qint32 i = x + 1; i < maxX; i++) { + qint32 ire = static_cast(fields[sourceNo][x + startOfLinePointer]); + if (ire > 200 || ire < 65335) { + endX = i; + } + } + + // Mark the dropout + for (qint32 i = startX; i < endX; i++) { + fieldDiff[sourceNo][i + startOfLinePointer] = 1; + } + + x = x + range; + } + } + } + } +} + +void DiffDod::performLumaFilter(QVector &fields, + LdDecodeMetaData::VideoParameters videoParameters, + QVector availableSourcesForFrame) +{ + // Filter out the chroma information from the fields leaving just luma + Filters filters; + + for (qint32 sourcePointer = 0; sourcePointer < availableSourcesForFrame.size(); sourcePointer++) { + qint32 sourceNo = availableSourcesForFrame[sourcePointer]; // Get the actual source + if (videoParameters.isSourcePal) { + filters.palLumaFirFilter(fields[sourceNo].data(), videoParameters.fieldWidth * videoParameters.fieldHeight); + } else { + filters.ntscLumaFirFilter(fields[sourceNo].data(), videoParameters.fieldWidth * videoParameters.fieldHeight); + } + } +} + +// Create an error map of the fields based on median value differential analysis +// Note: This only functions within the colour burst and visible areas of the frame +void DiffDod::getFieldErrorByMedian(QVector &fields, QVector &fieldDiff, + qint32 dodThreshold, + LdDecodeMetaData::VideoParameters videoParameters, + QVector availableSourcesForFrame) +{ + // This method requires at least three source frames + if (availableSourcesForFrame.size() < 3) { + return; + } + + // Normalize the % dodThreshold to 0.00-1.00 + float threshold = static_cast(dodThreshold) / 100.0; + + // Calculate the linear threshold for the colourburst region + qint32 cbThreshold = ((65535 / 100) * dodThreshold) / 4; // Note: The /4 is just a guess + + for (qint32 y = 0; y < videoParameters.fieldHeight; y++) { + qint32 startOfLinePointer = y * videoParameters.fieldWidth; + for (qint32 x = videoParameters.colourBurstStart; x < videoParameters.activeVideoEnd; x++) { + // Get the dot value from all of the sources + QVector dotValues(fields.size()); + for (qint32 sourcePointer = 0; sourcePointer < availableSourcesForFrame.size(); sourcePointer++) { + qint32 sourceNo = availableSourcesForFrame[sourcePointer]; // Get the actual source + dotValues[sourceNo] = static_cast(fields[sourceNo][x + startOfLinePointer]); + } + + // If we are in the visible area use Rec.709 logarithmic comparison + if (x >= videoParameters.activeVideoStart && x < videoParameters.activeVideoEnd) { + // Compute the median of the dot values + float vMedian = static_cast(convertLinearToBrightness(median(dotValues), videoParameters.black16bIre, videoParameters.white16bIre, videoParameters.isSourcePal)); + + for (qint32 sourcePointer = 0; sourcePointer < availableSourcesForFrame.size(); sourcePointer++) { + qint32 sourceNo = availableSourcesForFrame[sourcePointer]; // Get the actual source + float v = convertLinearToBrightness(dotValues[sourceNo], videoParameters.black16bIre, videoParameters.white16bIre, videoParameters.isSourcePal); + if ((v - vMedian) > threshold) fieldDiff[sourceNo][x + startOfLinePointer] = 2; + } + } + + // If we are in the colourburst use linear comparison + if (x >= videoParameters.colourBurstStart && x < videoParameters.colourBurstEnd) { + // We are in the colour burst, use linear comparison + qint32 dotMedian = median(dotValues); + for (qint32 sourcePointer = 0; sourcePointer < availableSourcesForFrame.size(); sourcePointer++) { + qint32 sourceNo = availableSourcesForFrame[sourcePointer]; // Get the actual source + if ((dotValues[sourceNo] - dotMedian) > cbThreshold) fieldDiff[sourceNo][x + startOfLinePointer] = 2; + } + } + } } +} + +// Method to create the field drop-out metadata based on the differential map of the fields +// This method compares each available source against all other available sources to determine where the source differs. +// If any of the frame's contents do not match that of the other sources, the frame's pixels are marked as dropouts. +QVector DiffDod::getFieldDropouts(QVector &fieldDiff, + LdDecodeMetaData::VideoParameters videoParameters, + QVector availableSourcesForFrame) +{ + // Create and resize the return data vector + QVector fieldDropouts; + fieldDropouts.resize(fieldDiff.size()); - // Show disc and video information - qInfo() << ""; - qInfo() << "Sources have VBI frame number range of" << tbcSources.getMinimumVbiFrameNumber() << "to" << tbcSources.getMaximumVbiFrameNumber(); - - // Check start and length - qint32 vbiStartFrame = startVbi; - if (vbiStartFrame < tbcSources.getMinimumVbiFrameNumber()) - vbiStartFrame = tbcSources.getMinimumVbiFrameNumber(); - - qint32 length = lengthVbi; - if (length > (tbcSources.getMaximumVbiFrameNumber() - vbiStartFrame + 1)) - length = tbcSources.getMaximumVbiFrameNumber() - vbiStartFrame + 1; - if (length == -1) length = tbcSources.getMaximumVbiFrameNumber() - tbcSources.getMinimumVbiFrameNumber() + 1; - - // Verify frame source availablity - qInfo() << ""; - qInfo() << "Verifying VBI frame multi-source availablity:"; - tbcSources.verifySources(vbiStartFrame, length); - - qInfo() << "Processing" << length << "frames starting from VBI frame" << vbiStartFrame; - if (!tbcSources.saveSources(vbiStartFrame, length, dodThreshold, lumaClip)) { - qCritical() << "Saving source failed!"; - return false; + // This method requires at least three source frames + if (availableSourcesForFrame.size() < 3) { + return fieldDropouts; } - return true; + // Define the area in which DOD should be performed + qint32 areaStart = videoParameters.colourBurstStart; + qint32 areaEnd = videoParameters.activeVideoEnd; + + // Process the frame one line at a time (both fields) + for (qint32 y = 0; y < videoParameters.fieldHeight; y++) { + qint32 startOfLinePointer = y * videoParameters.fieldWidth; + + // Process each source line in turn + for (qint32 sourcePointer = 0; sourcePointer < availableSourcesForFrame.size(); sourcePointer++) { + qint32 sourceNo = availableSourcesForFrame[sourcePointer]; // Get the actual source + + // Mark the individual dropouts + qint32 doCounter = 0; + qint32 minimumDetectLength = 5; + + qint32 doStart = 0; + qint32 doFieldLine = 0; + + // Only create dropouts between the start of the colour burst and the end of the + // active video area + for (qint32 x = areaStart; x < areaEnd; x++) { + // Compare field dot to threshold + if (static_cast(fieldDiff[sourceNo][x + startOfLinePointer]) == 0) { + // Current X is not a dropout + if (doCounter > 0) { + doCounter--; + if (doCounter == 0) { + // Mark the previous x as the end of the dropout + fieldDropouts[sourceNo].startx.append(doStart); + fieldDropouts[sourceNo].endx.append(x - 1); + fieldDropouts[sourceNo].fieldLine.append(doFieldLine); + } + } + } else { + // Current X is a dropout + if (doCounter == 0) { + doCounter = minimumDetectLength; + doStart = x; + doFieldLine = y + 1; + } + } + } + + // Ensure metadata dropouts end at the end of the active video area + if (doCounter > 0) { + doCounter = 0; + + fieldDropouts[sourceNo].startx.append(doStart); + fieldDropouts[sourceNo].endx.append(areaEnd); + fieldDropouts[sourceNo].fieldLine.append(doFieldLine); + } + + } // Next source + } // Next line + + return fieldDropouts; } -bool Diffdod::loadInputTbcFiles(QVector inputFilenames, bool reverse) +// Method to concatenate dropouts on the same line that are close together +// (to cut down on the amount of generated metadata with noisy/bad sources) +void DiffDod::concatenateFieldDropouts(QVector &dropouts, + QVector availableSourcesForFrame) { - for (qint32 i = 0; i < inputFilenames.size(); i++) { - qInfo().nospace() << "Loading TBC input source #" << i << " - Filename: " << inputFilenames[i]; - if (!tbcSources.loadSource(inputFilenames[i], reverse)) { - return false; + // This variable controls the minimum allowed gap between dropouts + // if the gap between the end of the last dropout and the start of + // the next is less than minimumGap, the two dropouts will be + // concatenated together + qint32 minimumGap = 50; + + for (qint32 sourcePointer = 0; sourcePointer < availableSourcesForFrame.size(); sourcePointer++) { + qint32 sourceNo = availableSourcesForFrame[sourcePointer]; // Get the actual source + + // Start from 1 as 0 has no previous dropout + qint32 i = 1; + + while (i < dropouts[sourceNo].startx.size()) { + // Is the current dropout on the same field line as the last? + if (dropouts[sourceNo].fieldLine[i - 1] == dropouts[sourceNo].fieldLine[i]) { + if ((dropouts[sourceNo].endx[i - 1] + minimumGap) > (dropouts[sourceNo].startx[i])) { + // Concatenate + dropouts[sourceNo].endx[i - 1] = dropouts[sourceNo].endx[i]; + + // Remove the current dropout + dropouts[sourceNo].startx.removeAt(i); + dropouts[sourceNo].endx.removeAt(i); + dropouts[sourceNo].fieldLine.removeAt(i); + } + } + + // Next dropout + i++; } } +} - return true; +// Method to find the median of a vector of qint32s +qint32 DiffDod::median(QVector v) +{ + size_t n = v.size() / 2; + std::nth_element(v.begin(), v.begin()+n, v.end()); + return v[n]; } + +// Method to convert a linear IRE to a logarithmic reflective brightness % +// Note: Follows the Rec. 709 OETF transfer function +float DiffDod::convertLinearToBrightness(quint16 value, quint16 black16bIre, quint16 white16bIre, bool isSourcePal) +{ + float v = 0; + float l = static_cast(value); + + // Factors to scale Y according to the black to white interval + // (i.e. make the black level 0 and the white level 65535) + float yScale = (1.0 / (white16bIre - black16bIre)) * 65535; + + if (!isSourcePal) { + // NTSC uses a 75% white point; so here we scale the result by + // 25% (making 100 IRE 25% over the maximum allowed white point) + yScale *= 125.0 / 100.0; + } + + // Scale the L to 0-65535 where 0 = blackIreLevel and 65535 = whiteIreLevel + l = (l - black16bIre) * yScale; + if (l > 65535) l = 65535; + if (l < 0) l = 0; + + // Scale L to 0.00-1.00 + l = (1.0 / 65535.0) * l; + + // Rec. 709 - https://en.wikipedia.org/wiki/Rec._709#Transfer_characteristics + if (l < 0.018) { + v = 4.500 * l; + } else { + v = pow(1.099 * l, 0.45) - 0.099; + } + + return v; +} + + + + + + + + + + + + + + + + + diff --git a/tools/ld-diffdod/diffdod.h b/tools/ld-diffdod/diffdod.h index 5ccf88511..9985acfbf 100644 --- a/tools/ld-diffdod/diffdod.h +++ b/tools/ld-diffdod/diffdod.h @@ -26,24 +26,52 @@ #define DIFFDOD_H #include +#include +#include +#include #include -#include +#include -#include "tbcsources.h" +// TBC library includes +#include "sourcevideo.h" +#include "lddecodemetadata.h" +#include "vbidecoder.h" +#include "filters.h" -class Diffdod : public QObject +class Sources; + +class DiffDod : public QThread { Q_OBJECT public: - explicit Diffdod(QObject *parent = nullptr); + explicit DiffDod(QAtomicInt& abort, Sources& sources, QObject *parent = nullptr); - bool process(QVector inputFilenames, bool reverse, - qint32 dodThreshold, bool lumaClip, qint32 startVbi, qint32 lengthVbi); +protected: + void run() override; private: - TbcSources tbcSources; + // Decoder pool + QAtomicInt& m_abort; + Sources& m_sources; + + // Processing methods + void performClipCheck(QVector &fields, QVector &fieldDiff, + LdDecodeMetaData::VideoParameters videoParameters, + QVector availableSourcesForFrame); + void performLumaFilter(QVector &fields, + LdDecodeMetaData::VideoParameters videoParameters, + QVector availableSourcesForFrame); + void getFieldErrorByMedian(QVector &fields, QVector &fieldDiff, qint32 dodThreshold, + LdDecodeMetaData::VideoParameters videoParameters, + QVector availableSourcesForFrame); + QVector getFieldDropouts(QVector &fieldDiff, + LdDecodeMetaData::VideoParameters videoParameters, + QVector availableSourcesForFrame); + + void concatenateFieldDropouts(QVector &dropouts, QVector availableSourcesForFrame); - bool loadInputTbcFiles(QVector inputFilenames, bool reverse); + qint32 median(QVector v); + float convertLinearToBrightness(quint16 value, quint16 black16bIre, quint16 white16bIre, bool isSourcePal); }; #endif // DIFFDOD_H diff --git a/tools/ld-diffdod/ld-diffdod.pro b/tools/ld-diffdod/ld-diffdod.pro index 851519e85..6a618aa65 100644 --- a/tools/ld-diffdod/ld-diffdod.pro +++ b/tools/ld-diffdod/ld-diffdod.pro @@ -22,7 +22,7 @@ SOURCES += \ ../library/tbc/logging.cpp \ diffdod.cpp \ main.cpp \ - tbcsources.cpp + sources.cpp HEADERS += \ ../library/filter/firfilter.h \ @@ -32,7 +32,7 @@ HEADERS += \ ../library/tbc/filters.h \ ../library/tbc/logging.h \ diffdod.h \ - tbcsources.h + sources.h # Add external includes to the include path INCLUDEPATH += ../library/filter diff --git a/tools/ld-diffdod/main.cpp b/tools/ld-diffdod/main.cpp index dd95bd1b1..737ee4156 100644 --- a/tools/ld-diffdod/main.cpp +++ b/tools/ld-diffdod/main.cpp @@ -28,7 +28,7 @@ #include #include "logging.h" -#include "diffdod.h" +#include "sources.h" int main(int argc, char *argv[]) { @@ -61,14 +61,14 @@ int main(int argc, char *argv[]) QCoreApplication::translate("main", "Reverse the field order to second/first (default first/second)")); parser.addOption(setReverseOption); - // Option to turn off luma clip detection (-n / --lumaclip) - QCommandLineOption setLumaOption(QStringList() << "n" << "lumaclip", - QCoreApplication::translate("main", "Perform luma clip signal dropout detection")); - parser.addOption(setLumaOption); + // Option to turn off signal clip detection (-n / --noclip) + QCommandLineOption setNoClipOption(QStringList() << "n" << "noclip", + QCoreApplication::translate("main", "Do not perform signal clip dropout detection")); + parser.addOption(setNoClipOption); // Option to select DOD threshold (-x / --dod-threshold) QCommandLineOption dodThresholdOption(QStringList() << "x" << "dod-threshold", - QCoreApplication::translate("main", "Specify the DOD threshold (100-65435 default: 400"), + QCoreApplication::translate("main", "Specify the DOD threshold percent (1 to 100% default: 7%"), QCoreApplication::translate("main", "number")); parser.addOption(dodThresholdOption); @@ -84,6 +84,13 @@ int main(int argc, char *argv[]) QCoreApplication::translate("main", "number")); parser.addOption(lengthVbiOption); + // Option to select the number of threads (-t) + QCommandLineOption threadsOption(QStringList() << "t" << "threads", + QCoreApplication::translate( + "main", "Specify the number of concurrent threads (default is the number of logical CPUs)"), + QCoreApplication::translate("main", "number")); + parser.addOption(threadsOption); + // Positional argument to specify input TBC files parser.addPositionalArgument("input", QCoreApplication::translate("main", "Specify input TBC files (minimum of 3)")); @@ -95,7 +102,8 @@ int main(int argc, char *argv[]) // Get the options from the parser bool reverse = parser.isSet(setReverseOption); - bool lumaClip = parser.isSet(setLumaOption); + bool signalClip = true; + if (parser.isSet(setNoClipOption)) signalClip = false; QVector inputFilenames; QStringList positionalArguments = parser.positionalArguments(); @@ -116,13 +124,13 @@ int main(int argc, char *argv[]) return -1; } - qint32 dodThreshold = 400; + qint32 dodThreshold = 7; if (parser.isSet(dodThresholdOption)) { dodThreshold = parser.value(dodThresholdOption).toInt(); - if (dodThreshold < 100 || dodThreshold > 65435) { + if (dodThreshold < 1 || dodThreshold > 100) { // Quit with error - qCritical("DOD threshold must be between 100 and 65435"); + qCritical("DOD threshold must be between 1 and 100 percent"); return -1; } } @@ -149,9 +157,21 @@ int main(int argc, char *argv[]) } } + qint32 maxThreads = QThread::idealThreadCount(); + if (parser.isSet(threadsOption)) { + maxThreads = parser.value(threadsOption).toInt(); + + if (maxThreads < 1) { + // Quit with error + qCritical("Specified number of threads must be greater than zero"); + return -1; + } + } + // Process the TBC file - Diffdod diffdod; - if (!diffdod.process(inputFilenames, reverse, dodThreshold, lumaClip, vbiFrameStart, vbiFrameLength)) { + Sources sources(inputFilenames, reverse, dodThreshold, signalClip, + vbiFrameStart, vbiFrameLength, maxThreads); + if (!sources.process()) { return 1; } diff --git a/tools/ld-diffdod/sources.cpp b/tools/ld-diffdod/sources.cpp new file mode 100644 index 000000000..e27cfa929 --- /dev/null +++ b/tools/ld-diffdod/sources.cpp @@ -0,0 +1,562 @@ +/************************************************************************ + + sources.cpp + + ld-diffdod - TBC Differential Drop-Out Detection tool + Copyright (C) 2019 Simon Inns + + This file is part of ld-decode-tools. + + ld-diffdod is free software: you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation, either version 3 of the + License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +************************************************************************/ + +#include "sources.h" + +Sources::Sources(QVector inputFilenames, bool reverse, + qint32 dodThreshold, bool signalClip, + qint32 startVbi, qint32 lengthVbi, + qint32 maxThreads, QObject *parent) + : QObject(parent), m_inputFilenames(inputFilenames), m_reverse(reverse), + m_dodThreshold(dodThreshold), m_signalClip(signalClip), m_startVbi(startVbi), + m_lengthVbi(lengthVbi), m_maxThreads(maxThreads) +{ + // Used to track the sources as they are loaded + currentSource = 0; +} + +bool Sources::process() +{ + // Show input filenames + qInfo() << "Processing" << m_inputFilenames.size() << "input TBC files:"; + for (qint32 i = 0; i < m_inputFilenames.size(); i++) qInfo().nospace() << " Source #" << i << ": " << m_inputFilenames[i]; + + // And then show the rest... + qInfo() << "Using" << m_maxThreads << "threads to process sources"; + if (m_reverse) qInfo() << "Using reverse field order"; else qInfo() << "Using normal field order"; + qInfo().nospace() << "Dropout detection threshold is " << m_dodThreshold << "% difference"; + if (m_signalClip) qInfo() << "Performing signal clip detection"; else qInfo() << "Not performing signal clip detection"; + qInfo() << ""; + + // Load the input TBC files --------------------------------------------------------------------------------------- + if (!loadInputTbcFiles(m_inputFilenames, m_reverse)) { + qCritical() << "Error: Unable to load input TBC files - cannot continue!"; + return false; + } + + // Show disc and video information + qInfo() << ""; + qInfo() << "Sources have VBI frame number range of" << getMinimumVbiFrameNumber() << + "to" << getMaximumVbiFrameNumber(); + + // Check start and length + qint32 vbiStartFrame = m_startVbi; + if (vbiStartFrame < getMinimumVbiFrameNumber()) + vbiStartFrame = getMinimumVbiFrameNumber(); + + qint32 length = m_lengthVbi; + if (length > (getMaximumVbiFrameNumber() - vbiStartFrame + 1)) + length = getMaximumVbiFrameNumber() - vbiStartFrame + 1; + if (length == -1) length = getMaximumVbiFrameNumber() - getMinimumVbiFrameNumber() + 1; + + // Verify frame source availablity + qInfo() << ""; + qInfo() << "Verifying VBI frame multi-source availablity..."; + verifySources(vbiStartFrame, length); + + // Process the sources -------------------------------------------------------------------------------------------- + inputFrameNumber = vbiStartFrame; + lastFrameNumber = vbiStartFrame + length; + processedFrames = 0; + + qInfo() << ""; + qInfo() << "Beginning multi-threaded diffDOD processing..."; + qInfo() << "Processing" << length << "frames - from VBI frame" << inputFrameNumber << "to" << lastFrameNumber; + totalTimer.start(); + + // Start a vector of decoding threads to process the video + qInfo() << "Beginning multi-threaded dropout correction process..."; + QVector threads; + threads.resize(m_maxThreads); + for (qint32 i = 0; i < m_maxThreads; i++) { + threads[i] = new DiffDod(abort, *this); + threads[i]->start(QThread::LowPriority); + } + + // Wait for the workers to finish + for (qint32 i = 0; i < m_maxThreads; i++) { + threads[i]->wait(); + delete threads[i]; + } + + // Did any of the threads abort? + if (abort) { + qCritical() << "Threads aborted! Cleaning up..."; + unloadInputTbcFiles(); + return false; + } + + // Show the processing speed to the user + float totalSecs = (static_cast(totalTimer.elapsed()) / 1000.0); + qInfo().nospace() << "DiffDOD complete - " << length << " frames in " << totalSecs << " seconds (" << + length / totalSecs << " FPS)"; + + // Save the sources ----------------------------------------------------------------------------------------------- + qInfo() << ""; + qInfo() << "Saving sources..."; + saveSources(); + + // Unload the input sources + qInfo() << ""; + qInfo() << "Cleaning up..."; + unloadInputTbcFiles(); + + return true; +} + +// Provide a frame to the threaded processing +bool Sources::getInputFrame(qint32& targetVbiFrame, + QVector& firstFields, QVector& secondFields, + LdDecodeMetaData::VideoParameters& videoParameters, + QVector& availableSourcesForFrame, + qint32& dodThreshold, bool& signalClip) +{ + QMutexLocker locker(&inputMutex); + + if (inputFrameNumber > lastFrameNumber) { + // No more input frames + return false; + } + + targetVbiFrame = inputFrameNumber; + inputFrameNumber++; + processedFrames++; + + // Get the metadata for the video parameters (all sources are the same, so just grab from the first) + videoParameters = sourceVideos[0]->ldDecodeMetaData.getVideoParameters(); + + // Get the number of available sources for the current frame + availableSourcesForFrame = getAvailableSourcesForFrame(targetVbiFrame); + + // Get the field data for the current frame (from all available sources) + firstFields = getFieldData(targetVbiFrame, true, availableSourcesForFrame); + secondFields = getFieldData(targetVbiFrame, false, availableSourcesForFrame); + + // Set the other miscellaneous parameters + dodThreshold = m_dodThreshold; + signalClip = m_signalClip; + + // User feedback + if (processedFrames % 100 == 0) qInfo() << "Processing frame" << targetVbiFrame; + + return true; +} + +// Receive a frame from the threaded processing +bool Sources::setOutputFrame(qint32 targetVbiFrame, + QVector firstFieldDropouts, + QVector secondFieldDropouts, + QVector availableSourcesForFrame) +{ + QMutexLocker locker(&outputMutex); + + // Write the first and second field line metadata back to the source + for (qint32 sourcePointer = 0; sourcePointer < availableSourcesForFrame.size(); sourcePointer++) { + qint32 sourceNo = availableSourcesForFrame[sourcePointer]; // Get the actual source + + // Get the required field numbers + qint32 firstFieldNumber = sourceVideos[sourceNo]-> + ldDecodeMetaData.getFirstFieldNumber(convertVbiFrameNumberToSequential(targetVbiFrame, sourceNo)); + qint32 secondFieldNumber = sourceVideos[sourceNo]-> + ldDecodeMetaData.getSecondFieldNumber(convertVbiFrameNumberToSequential(targetVbiFrame, sourceNo)); + + // Calculate the total number of dropouts detected for the frame + qint32 totalFirstDropouts = 0; + qint32 totalSecondDropouts = 0; + if (firstFieldDropouts.size() > 0) totalFirstDropouts = firstFieldDropouts[sourceNo].startx.size(); + if (secondFieldDropouts.size() > 0) totalSecondDropouts = secondFieldDropouts[sourceNo].startx.size(); + + qDebug() << "Writing source" << sourceNo << + "frame" << targetVbiFrame << "fields" << firstFieldNumber << "/" << secondFieldNumber << + "- Dropout records" << totalFirstDropouts << "/" << totalSecondDropouts; + + // Only replace the existing metadata if it was possible to create new metadata + if (availableSourcesForFrame.size() >= 3) { + // Remove the existing field dropout metadata for the field + sourceVideos[sourceNo]->ldDecodeMetaData.clearFieldDropOuts(firstFieldNumber); + sourceVideos[sourceNo]->ldDecodeMetaData.clearFieldDropOuts(secondFieldNumber); + + // Write the new field dropout metadata + sourceVideos[sourceNo]->ldDecodeMetaData.updateFieldDropOuts(firstFieldDropouts[sourceNo], firstFieldNumber); + sourceVideos[sourceNo]->ldDecodeMetaData.updateFieldDropOuts(secondFieldDropouts[sourceNo], secondFieldNumber); + } + } + + return true; +} + +// Load all available input sources +bool Sources::loadInputTbcFiles(QVector inputFilenames, bool reverse) +{ + for (qint32 i = 0; i < inputFilenames.size(); i++) { + qInfo().nospace() << "Loading TBC input source #" << i << " - Filename: " << inputFilenames[i]; + if (!loadSource(inputFilenames[i], reverse)) { + return false; + } + } + + return true; +} + +// Unload the input sources +void Sources::unloadInputTbcFiles() +{ + for (qint32 sourceNo = 0; sourceNo < getNumberOfAvailableSources(); sourceNo++) { + delete sourceVideos[sourceNo]; + sourceVideos.removeFirst(); + } +} + +// Load a TBC source video; returns false on failure +bool Sources::loadSource(QString filename, bool reverse) +{ + // Check that source file isn't already loaded + for (qint32 i = 0; i < sourceVideos.size(); i++) { + if (filename == sourceVideos[i]->filename) { + qCritical() << "Cannot load source - source is already loaded!"; + return false; + } + } + + bool loadSuccessful = true; + sourceVideos.resize(sourceVideos.size() + 1); + qint32 newSourceNumber = sourceVideos.size() - 1; + sourceVideos[newSourceNumber] = new Source; + LdDecodeMetaData::VideoParameters videoParameters; + + // Open the TBC metadata file + qInfo() << "Processing input TBC JSON metadata..."; + if (!sourceVideos[newSourceNumber]->ldDecodeMetaData.read(filename + ".json")) { + // Open failed + qWarning() << "Open TBC JSON metadata failed for filename" << filename; + qCritical() << "Cannot load source - JSON metadata could not be read!"; + + delete sourceVideos[newSourceNumber]; + sourceVideos.remove(newSourceNumber); + currentSource = 0; + return false; + } + + // Set the source as reverse field order if required + if (reverse) sourceVideos[newSourceNumber]->ldDecodeMetaData.setIsFirstFieldFirst(false); + + // Get the video parameters from the metadata + videoParameters = sourceVideos[newSourceNumber]->ldDecodeMetaData.getVideoParameters(); + + // Ensure that the TBC file has been mapped + if (!videoParameters.isMapped) { + qWarning() << "New source video has not been mapped!"; + qCritical() << "Cannot load source - The TBC has not been mapped (please run ld-discmap on the source)!"; + loadSuccessful = false; + } + + // Ensure that the video standard matches any existing sources + if (loadSuccessful) { + if ((sourceVideos.size() - 1 > 0) && (sourceVideos[0]->ldDecodeMetaData.getVideoParameters().isSourcePal + != videoParameters.isSourcePal)) { + qWarning() << "New source video standard does not match existing source(s)!"; + qCritical() << "Cannot load source - Mixing PAL and NTSC sources is not supported!"; + loadSuccessful = false; + } + } + + if (videoParameters.isSourcePal) qInfo() << "Video format is PAL"; else qInfo() << "Video format is NTSC"; + + // Ensure that the video has VBI data + if (loadSuccessful) { + if (!sourceVideos[newSourceNumber]->ldDecodeMetaData.getFieldVbi(1).inUse) { + qWarning() << "New source video does not contain VBI data!"; + qCritical() << "Cannot load source - No VBI data available. Please run ld-process-vbi before loading source!"; + loadSuccessful = false; + } + } + + // Determine the minimum and maximum VBI frame number and the disc type + if (loadSuccessful) { + qInfo() << "Determining input TBC disc type and VBI frame range..."; + if (!setDiscTypeAndMaxMinFrameVbi(newSourceNumber)) { + // Failed + qCritical() << "Cannot load source - Could not determine disc type and/or VBI frame range!"; + loadSuccessful = false; + } + } + + // Show the 0 and 100IRE points for the source + qInfo() << "Source has 0IRE at" << videoParameters.black16bIre << "and 100IRE at" << videoParameters.white16bIre; + + // Open the new source TBC video + if (loadSuccessful) { + qInfo() << "Loading input TBC video data..."; + if (!sourceVideos[newSourceNumber]->sourceVideo.open(filename, videoParameters.fieldWidth * videoParameters.fieldHeight)) { + // Open failed + qWarning() << "Open TBC file failed for filename" << filename; + qCritical() << "Cannot load source - Error reading source TBC data file!"; + loadSuccessful = false; + } + } + + // Finish up + if (loadSuccessful) { + // Loading successful + sourceVideos[newSourceNumber]->filename = filename; + loadSuccessful = true; + } else { + // Loading unsuccessful - Remove the new source entry and default the current source + sourceVideos[newSourceNumber]->sourceVideo.close(); + delete sourceVideos[newSourceNumber]; + sourceVideos.remove(newSourceNumber); + currentSource = 0; + return false; + } + + // Select the new source + currentSource = newSourceNumber; + + return true; +} + +// Method to work out the disc type (CAV or CLV) and the maximum and minimum +// VBI frame numbers for the source +bool Sources::setDiscTypeAndMaxMinFrameVbi(qint32 sourceNumber) +{ + sourceVideos[sourceNumber]->isSourceCav = false; + + // Determine the disc type + VbiDecoder vbiDecoder; + qint32 cavCount = 0; + qint32 clvCount = 0; + + qint32 typeCountMax = 100; + if (sourceVideos[sourceNumber]->ldDecodeMetaData.getNumberOfFrames() < typeCountMax) + typeCountMax = sourceVideos[sourceNumber]->ldDecodeMetaData.getNumberOfFrames(); + + // Using sequential frame numbering starting from 1 + for (qint32 seqFrame = 1; seqFrame <= typeCountMax; seqFrame++) { + // Get the VBI data and then decode + QVector vbi1 = sourceVideos[sourceNumber]->ldDecodeMetaData.getFieldVbi(sourceVideos[sourceNumber]-> + ldDecodeMetaData.getFirstFieldNumber(seqFrame)).vbiData; + QVector vbi2 = sourceVideos[sourceNumber]->ldDecodeMetaData.getFieldVbi(sourceVideos[sourceNumber]-> + ldDecodeMetaData.getSecondFieldNumber(seqFrame)).vbiData; + VbiDecoder::Vbi vbi = vbiDecoder.decodeFrame(vbi1[0], vbi1[1], vbi1[2], vbi2[0], vbi2[1], vbi2[2]); + + // Look for a complete, valid CAV picture number or CLV time-code + if (vbi.picNo > 0) cavCount++; + + if (vbi.clvHr != -1 && vbi.clvMin != -1 && + vbi.clvSec != -1 && vbi.clvPicNo != -1) clvCount++; + } + qDebug() << "Got" << cavCount << "CAV picture codes and" << clvCount << "CLV timecodes"; + + // If the metadata has no picture numbers or time-codes, we cannot use the source + if (cavCount == 0 && clvCount == 0) { + qDebug() << "Source does not seem to contain valid CAV picture numbers or CLV time-codes - cannot process"; + return false; + } + + // Determine disc type + if (cavCount > clvCount) { + sourceVideos[sourceNumber]->isSourceCav = true; + qDebug() << "Got" << cavCount << "valid CAV picture numbers - source disc type is CAV"; + qInfo() << "Disc type is CAV"; + } else { + sourceVideos[sourceNumber]->isSourceCav = false; + qDebug() << "Got" << clvCount << "valid CLV picture numbers - source disc type is CLV"; + qInfo() << "Disc type is CLV"; + + } + + // Disc has been mapped, so we can use the first and last frame numbers as the + // min and max range of VBI frame numbers in the input source + QVector vbi1 = sourceVideos[sourceNumber]->ldDecodeMetaData.getFieldVbi(sourceVideos[sourceNumber]-> + ldDecodeMetaData.getFirstFieldNumber(1)).vbiData; + QVector vbi2 = sourceVideos[sourceNumber]->ldDecodeMetaData.getFieldVbi(sourceVideos[sourceNumber]-> + ldDecodeMetaData.getSecondFieldNumber(1)).vbiData; + VbiDecoder::Vbi vbi = vbiDecoder.decodeFrame(vbi1[0], vbi1[1], vbi1[2], vbi2[0], vbi2[1], vbi2[2]); + + if (sourceVideos[sourceNumber]->isSourceCav) { + sourceVideos[sourceNumber]->minimumVbiFrameNumber = vbi.picNo; + } else { + LdDecodeMetaData::ClvTimecode timecode; + timecode.hours = vbi.clvHr; + timecode.minutes = vbi.clvMin; + timecode.seconds = vbi.clvSec; + timecode.pictureNumber = vbi.clvPicNo; + sourceVideos[sourceNumber]->minimumVbiFrameNumber = sourceVideos[sourceNumber]->ldDecodeMetaData.convertClvTimecodeToFrameNumber(timecode); + } + + vbi1 = sourceVideos[sourceNumber]->ldDecodeMetaData.getFieldVbi(sourceVideos[sourceNumber]-> + ldDecodeMetaData.getFirstFieldNumber(sourceVideos[sourceNumber]->ldDecodeMetaData.getNumberOfFrames())).vbiData; + vbi2 = sourceVideos[sourceNumber]->ldDecodeMetaData.getFieldVbi(sourceVideos[sourceNumber]-> + ldDecodeMetaData.getSecondFieldNumber(sourceVideos[sourceNumber]->ldDecodeMetaData.getNumberOfFrames())).vbiData; + vbi = vbiDecoder.decodeFrame(vbi1[0], vbi1[1], vbi1[2], vbi2[0], vbi2[1], vbi2[2]); + + if (sourceVideos[sourceNumber]->isSourceCav) { + sourceVideos[sourceNumber]->maximumVbiFrameNumber = vbi.picNo; + } else { + LdDecodeMetaData::ClvTimecode timecode; + timecode.hours = vbi.clvHr; + timecode.minutes = vbi.clvMin; + timecode.seconds = vbi.clvSec; + timecode.pictureNumber = vbi.clvPicNo; + sourceVideos[sourceNumber]->maximumVbiFrameNumber = sourceVideos[sourceNumber]->ldDecodeMetaData.convertClvTimecodeToFrameNumber(timecode); + } + + if (sourceVideos[sourceNumber]->isSourceCav) { + // If the source is CAV frame numbering should be a minimum of 1 (it + // can be 0 for CLV sources) + if (sourceVideos[sourceNumber]->minimumVbiFrameNumber < 1) { + qCritical() << "CAV start frame of" << sourceVideos[sourceNumber]->minimumVbiFrameNumber << "is out of bounds (should be 1 or above)"; + return false; + } + } + + qInfo() << "VBI frame number range is" << sourceVideos[sourceNumber]->minimumVbiFrameNumber << "to" << + sourceVideos[sourceNumber]->maximumVbiFrameNumber; + + return true; +} + +// Get the minimum VBI frame number for all sources +qint32 Sources::getMinimumVbiFrameNumber() +{ + qint32 minimumFrameNumber = 1000000; + for (qint32 i = 0; i < sourceVideos.size(); i++) { + if (sourceVideos[i]->minimumVbiFrameNumber < minimumFrameNumber) + minimumFrameNumber = sourceVideos[i]->minimumVbiFrameNumber; + } + + return minimumFrameNumber; +} + +// Get the maximum VBI frame number for all sources +qint32 Sources::getMaximumVbiFrameNumber() +{ + qint32 maximumFrameNumber = 0; + for (qint32 i = 0; i < sourceVideos.size(); i++) { + if (sourceVideos[i]->maximumVbiFrameNumber > maximumFrameNumber) + maximumFrameNumber = sourceVideos[i]->maximumVbiFrameNumber; + } + + return maximumFrameNumber; +} + +// Verify that at least 3 sources are available for every VBI frame +void Sources::verifySources(qint32 vbiStartFrame, qint32 length) +{ + qint32 uncorrectableFrameCount = 0; + + // Process the sources frame by frame + for (qint32 vbiFrame = vbiStartFrame; vbiFrame < vbiStartFrame + length; vbiFrame++) { + // Check how many source frames are available for the current frame + QVector availableSourcesForFrame = getAvailableSourcesForFrame(vbiFrame); + + // DiffDOD requires at least three source frames. If 3 sources are not available leave any existing DO records in place + // and output a warning to the user + if (availableSourcesForFrame.size() < 3) { + // Differential DOD requires at least 3 valid source frames + qDebug().nospace() << "Frame #" << vbiFrame << " has only " << availableSourcesForFrame.size() << " source frames available - cannot correct"; + uncorrectableFrameCount++; + } + } + + if (uncorrectableFrameCount != 0) { + qInfo() << "Warning:" << uncorrectableFrameCount << "frame(s) cannot be corrected!"; + } else { + qInfo() << "All frames have at least 3 sources available"; + } +} + +// Method that returns a vector of the sources that contain data for the required VBI frame number +QVector Sources::getAvailableSourcesForFrame(qint32 vbiFrameNumber) +{ + QVector availableSourcesForFrame; + for (qint32 sourceNo = 0; sourceNo < sourceVideos.size(); sourceNo++) { + if (vbiFrameNumber >= sourceVideos[sourceNo]->minimumVbiFrameNumber && vbiFrameNumber <= sourceVideos[sourceNo]->maximumVbiFrameNumber) { + // Get the field numbers for the frame + qint32 firstFieldNumber = sourceVideos[sourceNo]->ldDecodeMetaData.getFirstFieldNumber(convertVbiFrameNumberToSequential(vbiFrameNumber, sourceNo)); + qint32 secondFieldNumber = sourceVideos[sourceNo]->ldDecodeMetaData.getSecondFieldNumber(convertVbiFrameNumberToSequential(vbiFrameNumber, sourceNo)); + + // Ensure the frame is not a padded field (i.e. missing) + if (!(sourceVideos[sourceNo]->ldDecodeMetaData.getField(firstFieldNumber).pad && + sourceVideos[sourceNo]->ldDecodeMetaData.getField(secondFieldNumber).pad)) { + availableSourcesForFrame.append(sourceNo); + } + } + } + + return availableSourcesForFrame; +} + +// Method to convert a VBI frame number to a sequential frame number +qint32 Sources::convertVbiFrameNumberToSequential(qint32 vbiFrameNumber, qint32 sourceNumber) +{ + // Offset the VBI frame number to get the sequential source frame number + return vbiFrameNumber - sourceVideos[sourceNumber]->minimumVbiFrameNumber + 1; +} + +// Get the number of available sources +qint32 Sources::getNumberOfAvailableSources() +{ + return sourceVideos.size(); +} + +// Method to write the source metadata to disc +void Sources::saveSources() +{ + // Save the sources' metadata + for (qint32 sourceNo = 0; sourceNo < sourceVideos.size(); sourceNo++) { + // Write the JSON metadata + qInfo() << "Writing JSON metadata file for TBC file" << sourceNo; + sourceVideos[sourceNo]->ldDecodeMetaData.write(sourceVideos[sourceNo]->filename + ".json"); + } +} + +// Get the field data for the specified frame +QVector Sources::getFieldData(qint32 targetVbiFrame, bool isFirstField, + QVector &availableSourcesForFrame) +{ + // Only display on first field (otherwise we will get 2 of the same debug) + if (isFirstField) qDebug() << "Processing VBI Frame" << targetVbiFrame << "-" << availableSourcesForFrame.size() << "sources available"; + + // Get the field data for the frame from all of the available sources and copy locally + QVector fields; + fields.resize(getNumberOfAvailableSources()); + + QVector sourceFirstFieldPointer; + sourceFirstFieldPointer.resize(getNumberOfAvailableSources()); + + for (qint32 sourcePointer = 0; sourcePointer < availableSourcesForFrame.size(); sourcePointer++) { + qint32 sourceNo = availableSourcesForFrame[sourcePointer]; // Get the actual source + qint32 fieldNumber = -1; + if (isFirstField) fieldNumber = sourceVideos[sourceNo]-> + ldDecodeMetaData.getFirstFieldNumber(convertVbiFrameNumberToSequential(targetVbiFrame, sourceNo)); + else fieldNumber = sourceVideos[sourceNo]-> + ldDecodeMetaData.getSecondFieldNumber(convertVbiFrameNumberToSequential(targetVbiFrame, sourceNo)); + + // Copy the data locally + fields[sourceNo] = (sourceVideos[sourceNo]->sourceVideo.getVideoField(fieldNumber)); + } + + return fields; +} diff --git a/tools/ld-diffdod/sources.h b/tools/ld-diffdod/sources.h new file mode 100644 index 000000000..f576fa96a --- /dev/null +++ b/tools/ld-diffdod/sources.h @@ -0,0 +1,116 @@ +/************************************************************************ + + sources.h + + ld-diffdod - TBC Differential Drop-Out Detection tool + Copyright (C) 2019 Simon Inns + + This file is part of ld-decode-tools. + + ld-diffdod is free software: you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation, either version 3 of the + License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +************************************************************************/ + +#ifndef SOURCES_H +#define SOURCES_H + +#include +#include + +#include +#include +#include +#include +#include + +#include "diffdod.h" + +class Sources : public QObject +{ + Q_OBJECT + +public: + explicit Sources(QVector inputFilenames, bool reverse, + qint32 dodThreshold, bool lumaClip, + qint32 startVbi, qint32 lengthVbi, + qint32 maxThreads, QObject *parent = nullptr); + + bool process(); + + // Member functions used by worker threads + bool getInputFrame(qint32& targetVbiFrame, + QVector& firstFields, QVector& secondFields, + LdDecodeMetaData::VideoParameters& videoParameters, + QVector& availableSourcesForFrame, + qint32& dodThreshold, bool& signalClip); + + bool setOutputFrame(qint32 targetVbiFrame, + QVector firstFieldDropouts, + QVector secondFieldDropouts, + QVector availableSourcesForFrame); + +private: + // Source definition + struct Source { + SourceVideo sourceVideo; + LdDecodeMetaData ldDecodeMetaData; + QString filename; + qint32 minimumVbiFrameNumber; + qint32 maximumVbiFrameNumber; + bool isSourceCav; + }; + + QVector sourceVideos; + qint32 currentSource; + QElapsedTimer totalTimer; + qint32 processedFrames; + + // Setup variables + QVector m_inputFilenames; + bool m_reverse; + qint32 m_dodThreshold; + bool m_signalClip; + qint32 m_startVbi; + qint32 m_lengthVbi; + qint32 m_maxThreads; + + // Input stream variables (all guarded by inputMutex while threads are running) + QMutex inputMutex; + qint32 inputFrameNumber; + qint32 lastFrameNumber; + + // Output stream variables (all guarded by outputMutex while threads are running) + QMutex outputMutex; + + // Atomic abort flag shared by worker threads; workers watch this, and shut + // down as soon as possible if it becomes true + QAtomicInt abort; + + bool loadInputTbcFiles(QVector inputFilenames, bool reverse); + void unloadInputTbcFiles(); + bool loadSource(QString filename, bool reverse); + bool setDiscTypeAndMaxMinFrameVbi(qint32 sourceNumber); + qint32 getMinimumVbiFrameNumber(); + qint32 getMaximumVbiFrameNumber(); + void verifySources(qint32 vbiStartFrame, qint32 length); + QVector getAvailableSourcesForFrame(qint32 vbiFrameNumber); + qint32 convertVbiFrameNumberToSequential(qint32 vbiFrameNumber, qint32 sourceNumber); + qint32 getNumberOfAvailableSources(); + //void processSources(qint32 vbiStartFrame, qint32 length, qint32 dodThreshold, bool lumaClip); + void saveSources(); + QVector getFieldData(qint32 targetVbiFrame, bool isFirstField, + QVector &availableSourcesForFrame); +}; + +#endif // SOURCES_H diff --git a/tools/ld-diffdod/tbcsources.cpp b/tools/ld-diffdod/tbcsources.cpp deleted file mode 100644 index 4e4e00f35..000000000 --- a/tools/ld-diffdod/tbcsources.cpp +++ /dev/null @@ -1,705 +0,0 @@ -/************************************************************************ - - tbcsources.cpp - - ld-diffdod - TBC Differential Drop-Out Detection tool - Copyright (C) 2019 Simon Inns - - This file is part of ld-decode-tools. - - ld-diffdod is free software: you can redistribute it and/or - modify it under the terms of the GNU General Public License as - published by the Free Software Foundation, either version 3 of the - License, or (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -************************************************************************/ - -#include "tbcsources.h" - -TbcSources::TbcSources(QObject *parent) : QObject(parent) -{ - currentSource = 0; - currentVbiFrameNumber = 1; -} - -// Load a TBC source video; returns false on failure -bool TbcSources::loadSource(QString filename, bool reverse) -{ - // Check that source file isn't already loaded - for (qint32 i = 0; i < sourceVideos.size(); i++) { - if (filename == sourceVideos[i]->filename) { - qCritical() << "Cannot load source - source is already loaded!"; - return false; - } - } - - bool loadSuccessful = true; - sourceVideos.resize(sourceVideos.size() + 1); - qint32 newSourceNumber = sourceVideos.size() - 1; - sourceVideos[newSourceNumber] = new Source; - LdDecodeMetaData::VideoParameters videoParameters; - - // Open the TBC metadata file - qInfo() << "Processing input TBC JSON metadata..."; - if (!sourceVideos[newSourceNumber]->ldDecodeMetaData.read(filename + ".json")) { - // Open failed - qWarning() << "Open TBC JSON metadata failed for filename" << filename; - qCritical() << "Cannot load source - JSON metadata could not be read!"; - - delete sourceVideos[newSourceNumber]; - sourceVideos.remove(newSourceNumber); - currentSource = 0; - return false; - } - - // Set the source as reverse field order if required - if (reverse) sourceVideos[newSourceNumber]->ldDecodeMetaData.setIsFirstFieldFirst(false); - - // Get the video parameters from the metadata - videoParameters = sourceVideos[newSourceNumber]->ldDecodeMetaData.getVideoParameters(); - - // Ensure that the TBC file has been mapped - if (!videoParameters.isMapped) { - qWarning() << "New source video has not been mapped!"; - qCritical() << "Cannot load source - The TBC has not been mapped (please run ld-discmap on the source)!"; - loadSuccessful = false; - } - - // Ensure that the video standard matches any existing sources - if (loadSuccessful) { - if ((sourceVideos.size() - 1 > 0) && (sourceVideos[0]->ldDecodeMetaData.getVideoParameters().isSourcePal != videoParameters.isSourcePal)) { - qWarning() << "New source video standard does not match existing source(s)!"; - qCritical() << "Cannot load source - Mixing PAL and NTSC sources is not supported!"; - loadSuccessful = false; - } - } - - if (videoParameters.isSourcePal) qInfo() << "Video format is PAL"; else qInfo() << "Video format is NTSC"; - - // Ensure that the video has VBI data - if (loadSuccessful) { - if (!sourceVideos[newSourceNumber]->ldDecodeMetaData.getFieldVbi(1).inUse) { - qWarning() << "New source video does not contain VBI data!"; - qCritical() << "Cannot load source - No VBI data available. Please run ld-process-vbi before loading source!"; - loadSuccessful = false; - } - } - - // Determine the minimum and maximum VBI frame number and the disc type - if (loadSuccessful) { - qInfo() << "Determining input TBC disc type and VBI frame range..."; - if (!setDiscTypeAndMaxMinFrameVbi(newSourceNumber)) { - // Failed - qCritical() << "Cannot load source - Could not determine disc type and/or VBI frame range!"; - loadSuccessful = false; - } - } - - // Show the 0 and 100IRE points for the source - qInfo() << "Source has 0IRE at" << videoParameters.black16bIre << "and 100IRE at" << videoParameters.white16bIre; - - // Open the new source TBC video - if (loadSuccessful) { - qInfo() << "Loading input TBC video data..."; - if (!sourceVideos[newSourceNumber]->sourceVideo.open(filename, videoParameters.fieldWidth * videoParameters.fieldHeight)) { - // Open failed - qWarning() << "Open TBC file failed for filename" << filename; - qCritical() << "Cannot load source - Error reading source TBC data file!"; - loadSuccessful = false; - } - } - - // Finish up - if (loadSuccessful) { - // Loading successful - sourceVideos[newSourceNumber]->filename = filename; - loadSuccessful = true; - } else { - // Loading unsuccessful - Remove the new source entry and default the current source - sourceVideos[newSourceNumber]->sourceVideo.close(); - delete sourceVideos[newSourceNumber]; - sourceVideos.remove(newSourceNumber); - currentSource = 0; - return false; - } - - // Select the new source - currentSource = newSourceNumber; - - return true; -} - -// Unload a source video and remove it's data -bool TbcSources::unloadSource() -{ - sourceVideos[currentSource]->sourceVideo.close(); - delete sourceVideos[currentSource]; - sourceVideos.remove(currentSource); - currentSource = 0; - - return false; -} - -// Save TBC source video metadata for all sources; returns false on failure -bool TbcSources::saveSources(qint32 vbiStartFrame, qint32 length, qint32 dodThreshold, bool lumaClip) -{ - // Process the sources frame by frame - for (qint32 vbiFrame = vbiStartFrame; vbiFrame < vbiStartFrame + length; vbiFrame++) { - if ((vbiFrame % 100 == 0) || (vbiFrame == vbiStartFrame)) qInfo() << "Processing VBI frame" << vbiFrame; - - // Process - performFrameDiffDod(vbiFrame, dodThreshold, lumaClip); - } - - // Save the sources' metadata - for (qint32 sourceNo = 0; sourceNo < sourceVideos.size(); sourceNo++) { - // Write the JSON metadata - qInfo() << "Writing JSON metadata file for TBC file" << sourceNo; - sourceVideos[sourceNo]->ldDecodeMetaData.write(sourceVideos[sourceNo]->filename + ".json"); - } - - return true; -} - -// Get the number of available sources -qint32 TbcSources::getNumberOfAvailableSources() -{ - return sourceVideos.size(); -} - -// Get the minimum VBI frame number for all sources -qint32 TbcSources::getMinimumVbiFrameNumber() -{ - qint32 minimumFrameNumber = 1000000; - for (qint32 i = 0; i < sourceVideos.size(); i++) { - if (sourceVideos[i]->minimumVbiFrameNumber < minimumFrameNumber) - minimumFrameNumber = sourceVideos[i]->minimumVbiFrameNumber; - } - - return minimumFrameNumber; -} - -// Get the maximum VBI frame number for all sources -qint32 TbcSources::getMaximumVbiFrameNumber() -{ - qint32 maximumFrameNumber = 0; - for (qint32 i = 0; i < sourceVideos.size(); i++) { - if (sourceVideos[i]->maximumVbiFrameNumber > maximumFrameNumber) - maximumFrameNumber = sourceVideos[i]->maximumVbiFrameNumber; - } - - return maximumFrameNumber; -} - -// Verify that at least 3 sources are available for every VBI frame -void TbcSources::verifySources(qint32 vbiStartFrame, qint32 length) -{ - qint32 uncorrectableFrameCount = 0; - - // Process the sources frame by frame - for (qint32 vbiFrame = vbiStartFrame; vbiFrame < vbiStartFrame + length; vbiFrame++) { - // Check how many source frames are available for the current frame - QVector availableSourcesForFrame = getAvailableSourcesForFrame(vbiFrame); - - // DiffDOD requires at least three source frames. If 3 sources are not available leave any existing DO records in place - // and output a warning to the user - if (availableSourcesForFrame.size() < 3) { - // Differential DOD requires at least 3 valid source frames - qInfo().nospace() << "Frame #" << vbiFrame << " has only " << availableSourcesForFrame.size() << " source frames available - cannot correct"; - uncorrectableFrameCount++; - } - } - - if (uncorrectableFrameCount != 0) { - qInfo() << "Warning:" << uncorrectableFrameCount << "frame(s) cannot be corrected!"; - } else { - qInfo() << "All frames have at least 3 sources available"; - } -} - -// Private methods ---------------------------------------------------------------------------------------------------- - -// Perform differential dropout detection to determine (for each source) which frame pixels are valid -// Note: This method processes a single frame -void TbcSources::performFrameDiffDod(qint32 targetVbiFrame, qint32 dodThreshold, bool lumaClip) -{ - // Range check the diffDOD threshold - if (dodThreshold < 100) dodThreshold = 100; - if (dodThreshold > 65435) dodThreshold = 65435; - - // Get the field data for the current frame (from all available sources) - QVector firstFields = getFieldData(targetVbiFrame, true); - QVector secondFields = getFieldData(targetVbiFrame, false); - - // Create a differential map of the fields for the avaialble frames (based on the DOD threshold) - QVector> firstFieldsDiff = getFieldDiff(firstFields, dodThreshold); - QVector> secondFieldsDiff = getFieldDiff(secondFields, dodThreshold); - - // Perform luma clip check? - if (lumaClip) { - performLumaClip(firstFields, firstFieldsDiff); - performLumaClip(secondFields, secondFieldsDiff); - } - - // Create the drop-out metadata based on the differential map of the fields - QVector firstFieldDropouts = getFieldDropouts(firstFieldsDiff); - QVector secondFieldDropouts = getFieldDropouts(secondFieldsDiff); - - // Concatenate dropouts on the same line that are close together (to cut down on the - // amount of generated metadata with noisy/bad sources) - concatenateFieldDropouts(firstFieldDropouts); - concatenateFieldDropouts(secondFieldDropouts); - - // Write the dropout metadata back to the sources - writeDropoutMetadata(firstFieldDropouts, secondFieldDropouts, targetVbiFrame); -} - -// Get the field data for the specified frame -QVector TbcSources::getFieldData(qint32 targetVbiFrame, bool isFirstField) -{ - // Get the metadata for the video parameters (all sources are the same, so just grab from the first) - LdDecodeMetaData::VideoParameters videoParameters = sourceVideos[0]->ldDecodeMetaData.getVideoParameters(); - - // Check how many source frames are available for the current frame - QVector availableSourcesForFrame = getAvailableSourcesForFrame(targetVbiFrame); - - // DiffDOD requires at least three source frames. If 3 sources are not available leave any existing DO records in place - // and output a warning to the user - if (availableSourcesForFrame.size() < 3) { - // Differential DOD requires at least 3 valid source frames - qInfo() << "Only" << availableSourcesForFrame.size() << "source frames are available - can not perform diffDOD for VBI frame" << targetVbiFrame; - return QVector(); - } - qDebug() << "TbcSources::performFrameDiffDod(): Processing VBI Frame" << targetVbiFrame << "-" << availableSourcesForFrame.size() << "sources available"; - - // Get the field data for the frame from all of the available sources and copy locally - QVector fields; - fields.resize(availableSourcesForFrame.size()); - - QVector sourceFirstFieldPointer; - sourceFirstFieldPointer.resize(availableSourcesForFrame.size()); - - for (qint32 sourceNo = 0; sourceNo < availableSourcesForFrame.size(); sourceNo++) { - qint32 fieldNumber = -1; - if (isFirstField) fieldNumber = sourceVideos[availableSourcesForFrame[sourceNo]]-> - ldDecodeMetaData.getFirstFieldNumber(convertVbiFrameNumberToSequential(targetVbiFrame, availableSourcesForFrame[sourceNo])); - else fieldNumber = sourceVideos[availableSourcesForFrame[sourceNo]]-> - ldDecodeMetaData.getSecondFieldNumber(convertVbiFrameNumberToSequential(targetVbiFrame, availableSourcesForFrame[sourceNo])); - - // Copy the data locally - fields[sourceNo] = (sourceVideos[availableSourcesForFrame[sourceNo]]->sourceVideo.getVideoField(fieldNumber)); - - // Filter out the chroma information from the fields leaving just luma - Filters filters; - if (videoParameters.isSourcePal) { - filters.palLumaFirFilter(fields[sourceNo].data(), videoParameters.fieldWidth * videoParameters.fieldHeight); - } else { - filters.ntscLumaFirFilter(fields[sourceNo].data(), videoParameters.fieldWidth * videoParameters.fieldHeight); - } - - // Remove the existing field dropout metadata for the field - sourceVideos[availableSourcesForFrame[sourceNo]]->ldDecodeMetaData.clearFieldDropOuts(fieldNumber); - } - - return fields; -} - -// Create a differential map of the fields (this is a map of each dot in the field and how many -// other sources it differs from) -QVector> TbcSources::getFieldDiff(QVector &fields, qint32 dodThreshold) -{ - // Get the metadata for the video parameters (all sources are the same, so just grab from the first) - LdDecodeMetaData::VideoParameters videoParameters = sourceVideos[0]->ldDecodeMetaData.getVideoParameters(); - - // Make a vector to store the result of the diff - QVector> fieldDiff; - fieldDiff.resize(fields.size()); - - // Set the diff vector elements to zero (and resize the sub-vectors) - for (qint32 sourceCounter = 0; sourceCounter < fields.size(); sourceCounter++) { - fieldDiff[sourceCounter].fill(0, videoParameters.fieldHeight * videoParameters.fieldWidth); - } - - // Process the fields one line at a time - for (qint32 y = 0; y < videoParameters.fieldHeight; y++) { - qint32 startOfLinePointer = y * videoParameters.fieldWidth; - - // Compare all combinations of source and target field lines - // Note: the source is the field line we are building a DO map for, target is the field line we are - // comparing the source to. - for (qint32 sourceCounter = 0; sourceCounter < fields.size(); sourceCounter++) { - for (qint32 targetCounter = 0; targetCounter < fields.size(); targetCounter++) { - if (sourceCounter != targetCounter) { - for (qint32 x = 0; x < videoParameters.fieldWidth; x++) { - // Get the IRE values for target and source fields, cast to 32 bit signed - qint32 targetIre = static_cast(fields[targetCounter][x + startOfLinePointer]); - qint32 sourceIre = static_cast(fields[sourceCounter][x + startOfLinePointer]); - - // Diff the 16-bit pixel values of the first fields - qint32 difference = abs(targetIre - sourceIre); - - // If the source and target differ, increment the fieldDiff - if (difference > dodThreshold) fieldDiff[sourceCounter][x + startOfLinePointer] += 1; - } - } - } - } - } - - return fieldDiff; -} - -// Perform a luma clip check on the field -void TbcSources::performLumaClip(QVector &fields, QVector> &fieldsDiff) -{ - // Get the metadata for the video parameters (all sources are the same, so just grab from the first) - LdDecodeMetaData::VideoParameters videoParameters = sourceVideos[0]->ldDecodeMetaData.getVideoParameters(); - - // Process the fields one line at a time - for (qint32 y = videoParameters.firstActiveFieldLine; y < videoParameters.lastActiveFieldLine; y++) { - qint32 startOfLinePointer = y * videoParameters.fieldWidth; - - for (qint32 sourceCounter = 0; sourceCounter < fields.size(); sourceCounter++) { - // Set the clipping levels - qint32 blackClipLevel = videoParameters.black16bIre - 4000; - qint32 whiteClipLevel = videoParameters.white16bIre + 4000; - - for (qint32 x = videoParameters.activeVideoStart; x < videoParameters.activeVideoEnd; x++) { - // Get the IRE value for the source field, cast to 32 bit signed - qint32 sourceIre = static_cast(fields[sourceCounter][x + startOfLinePointer]); - - // Check for a luma clip event - if ((sourceIre < blackClipLevel) || (sourceIre > whiteClipLevel)) { - // Luma has clipped, scan back and forth looking for the start - // and end points of the event (i.e. the point where the event - // goes back into the expected IRE range) - qint32 range = 10; // maximum + and - scan range - qint32 minX = x - range; - if (minX < videoParameters.activeVideoStart) minX = videoParameters.activeVideoStart; - qint32 maxX = x + range; - if (maxX > videoParameters.activeVideoEnd) maxX = videoParameters.activeVideoEnd; - - qint32 startX = x; - qint32 endX = x; - - for (qint32 i = x; i > minX; i--) { - qint32 ire = static_cast(fields[sourceCounter][x + startOfLinePointer]); - if (ire < videoParameters.black16bIre || ire > videoParameters.white16bIre) { - startX = i; - } - } - - for (qint32 i = x+1; i < maxX; i++) { - qint32 ire = static_cast(fields[sourceCounter][x + startOfLinePointer]); - if (ire < videoParameters.black16bIre || ire > videoParameters.white16bIre) { - endX = i; - } - } - - // Mark the dropout - for (qint32 i = startX; i < endX; i++) { - fieldsDiff[sourceCounter][i + startOfLinePointer] += 255; - } - - x = x + range; - } - } - } - } -} - -// Method to create the field drop-out metadata based on the differential map of the fields -QVector TbcSources::getFieldDropouts(QVector> &fieldsDiff) -{ - // Get the metadata for the video parameters (all sources are the same, so just grab from the first) - LdDecodeMetaData::VideoParameters videoParameters = sourceVideos[0]->ldDecodeMetaData.getVideoParameters(); - - // This compares each available source against all other available sources to determine where the source differs. - // If any of the frame's contents do not match that of the other sources, the frame's pixels are marked as dropouts. - QVector frameDropouts; - frameDropouts.resize(fieldsDiff.size()); - - // Define the area in which DOD should be performed - qint32 areaStart = videoParameters.colourBurstStart; - qint32 areaEnd = videoParameters.activeVideoEnd; - - // Process the frame one line at a time (both fields) - for (qint32 y = 0; y < videoParameters.fieldHeight; y++) { - qint32 startOfLinePointer = y * videoParameters.fieldWidth; - - // Now the value of diffs[source].firstDiff[x]/diffs[source].secondDiff[x] contains the number of other target fields - // that differ from the source field line. - - // The minimum number of sources for diffDOD is 3, and when comparing 3 sources, each source has to - // match at least 2 other sources. As the sources increase, so does the required number of matches - // (i.e. for 4 sources, 3 should match and so on). This makes the diffDOD better and better as the - // number of available sources increase. - qint32 diffCompareThreshold = fieldsDiff.size() - 2; - - // Process each source line in turn - for (qint32 sourceNo = 0; sourceNo < fieldsDiff.size(); sourceNo++) { - // Mark the individual dropouts - qint32 doCounter = 0; - qint32 minimumDetectLength = 5; - - qint32 doStart = 0; - qint32 doFieldLine = 0; - - // Only create dropouts between the start of the colour burst and the end of the - // active video area - for (qint32 x = areaStart; x < areaEnd; x++) { - // Compare field dot to threshold - if (static_cast(fieldsDiff[sourceNo][x + startOfLinePointer]) <= diffCompareThreshold) { - // Current X is not a dropout - if (doCounter > 0) { - doCounter--; - if (doCounter == 0) { - // Mark the previous x as the end of the dropout - frameDropouts[sourceNo].startx.append(doStart); - frameDropouts[sourceNo].endx.append(x - 1); - frameDropouts[sourceNo].fieldLine.append(doFieldLine); - } - } - } else { - // Current X is a dropout - if (doCounter == 0) { - doCounter = minimumDetectLength; - doStart = x; - doFieldLine = y + 1; - } - } - } - - // Ensure metadata dropouts end at the end of the active video area - if (doCounter > 0) { - doCounter = 0; - - frameDropouts[sourceNo].startx.append(doStart); - frameDropouts[sourceNo].endx.append(areaEnd); - frameDropouts[sourceNo].fieldLine.append(doFieldLine); - } - - } // Next source - } // Next line - - return frameDropouts; -} - -void TbcSources::writeDropoutMetadata(QVector &firstFieldDropouts, - QVector &secondFieldDropouts, qint32 targetVbiFrame) -{ - // Check how many source frames are available for the current frame - QVector availableSourcesForFrame = getAvailableSourcesForFrame(targetVbiFrame); - - // Write the first and second field line metadata back to the source - QVector totalForSource(availableSourcesForFrame.size()); - for (qint32 sourceNo = 0; sourceNo < availableSourcesForFrame.size(); sourceNo++) { - // Get the required field numbers - qint32 firstFieldNumber = sourceVideos[availableSourcesForFrame[sourceNo]]-> - ldDecodeMetaData.getFirstFieldNumber(convertVbiFrameNumberToSequential(targetVbiFrame, availableSourcesForFrame[sourceNo])); - qint32 secondFieldNumber = sourceVideos[availableSourcesForFrame[sourceNo]]-> - ldDecodeMetaData.getSecondFieldNumber(convertVbiFrameNumberToSequential(targetVbiFrame, availableSourcesForFrame[sourceNo])); - - // Calculate the total number of dropouts detected for the frame - qint32 totalFirstDropouts = firstFieldDropouts[availableSourcesForFrame[sourceNo]].startx.size(); - qint32 totalSecondDropouts = secondFieldDropouts[availableSourcesForFrame[sourceNo]].startx.size(); - - qDebug() << "TbcSources::performFrameDiffDod(): Writing source" << availableSourcesForFrame[sourceNo] << - "frame" << targetVbiFrame << "fields" << firstFieldNumber << "/" << secondFieldNumber << - "- Dropout records" << totalFirstDropouts << "/" << totalSecondDropouts; - - // Record the total number of DOs for this source - totalForSource[sourceNo] = totalFirstDropouts + totalSecondDropouts; - - // Write the metadata - sourceVideos[availableSourcesForFrame[sourceNo]]->ldDecodeMetaData.updateFieldDropOuts(firstFieldDropouts[sourceNo], firstFieldNumber); - sourceVideos[availableSourcesForFrame[sourceNo]]->ldDecodeMetaData.updateFieldDropOuts(secondFieldDropouts[sourceNo], secondFieldNumber); - } -} - -// Method to concatenate dropouts on the same line that are close together -// (to cut down on the amount of generated metadata with noisy/bad sources) -void TbcSources::concatenateFieldDropouts(QVector &dropouts) -{ - // This variable controls the minimum allowed gap between dropouts - // if the gap between the end of the last dropout and the start of - // the next is less than minimumGap, the two dropouts will be - // concatenated together - qint32 minimumGap = 50; - - for (qint32 sourceNo = 0; sourceNo < dropouts.size(); sourceNo++) { - // Start from 1 as 0 has no previous dropout - qint32 i = 1; - while (i < dropouts[sourceNo].startx.size()) { - // Is the current dropout on the same field line as the last? - if (dropouts[sourceNo].fieldLine[i - 1] == dropouts[sourceNo].fieldLine[i]) { - if ((dropouts[sourceNo].endx[i - 1] + minimumGap) > (dropouts[sourceNo].startx[i])) { - // Concatenate - dropouts[sourceNo].endx[i - 1] = dropouts[sourceNo].endx[i]; - - // Remove the current dropout - dropouts[sourceNo].startx.removeAt(i); - dropouts[sourceNo].endx.removeAt(i); - dropouts[sourceNo].fieldLine.removeAt(i); - } - } - - // Next dropout - i++; - } - } -} - -// Method that returns a vector of the sources that contain data for the required VBI frame number -QVector TbcSources::getAvailableSourcesForFrame(qint32 vbiFrameNumber) -{ - QVector availableSourcesForFrame; - for (qint32 sourceNo = 0; sourceNo < sourceVideos.size(); sourceNo++) { - if (vbiFrameNumber >= sourceVideos[sourceNo]->minimumVbiFrameNumber && vbiFrameNumber <= sourceVideos[sourceNo]->maximumVbiFrameNumber) { - // Get the field numbers for the frame - qint32 firstFieldNumber = sourceVideos[sourceNo]->ldDecodeMetaData.getFirstFieldNumber(convertVbiFrameNumberToSequential(vbiFrameNumber, sourceNo)); - qint32 secondFieldNumber = sourceVideos[sourceNo]->ldDecodeMetaData.getSecondFieldNumber(convertVbiFrameNumberToSequential(vbiFrameNumber, sourceNo)); - - // Ensure the frame is not a padded field (i.e. missing) - if (!(sourceVideos[sourceNo]->ldDecodeMetaData.getField(firstFieldNumber).pad && - sourceVideos[sourceNo]->ldDecodeMetaData.getField(secondFieldNumber).pad)) { - availableSourcesForFrame.append(sourceNo); - } - } - } - - return availableSourcesForFrame; -} - -// Method to work out the disc type (CAV or CLV) and the maximum and minimum -// VBI frame numbers for the source -bool TbcSources::setDiscTypeAndMaxMinFrameVbi(qint32 sourceNumber) -{ - sourceVideos[sourceNumber]->isSourceCav = false; - - // Determine the disc type - VbiDecoder vbiDecoder; - qint32 cavCount = 0; - qint32 clvCount = 0; - - qint32 typeCountMax = 100; - if (sourceVideos[sourceNumber]->ldDecodeMetaData.getNumberOfFrames() < typeCountMax) - typeCountMax = sourceVideos[sourceNumber]->ldDecodeMetaData.getNumberOfFrames(); - - // Using sequential frame numbering starting from 1 - for (qint32 seqFrame = 1; seqFrame <= typeCountMax; seqFrame++) { - // Get the VBI data and then decode - QVector vbi1 = sourceVideos[sourceNumber]->ldDecodeMetaData.getFieldVbi(sourceVideos[sourceNumber]-> - ldDecodeMetaData.getFirstFieldNumber(seqFrame)).vbiData; - QVector vbi2 = sourceVideos[sourceNumber]->ldDecodeMetaData.getFieldVbi(sourceVideos[sourceNumber]-> - ldDecodeMetaData.getSecondFieldNumber(seqFrame)).vbiData; - VbiDecoder::Vbi vbi = vbiDecoder.decodeFrame(vbi1[0], vbi1[1], vbi1[2], vbi2[0], vbi2[1], vbi2[2]); - - // Look for a complete, valid CAV picture number or CLV time-code - if (vbi.picNo > 0) cavCount++; - - if (vbi.clvHr != -1 && vbi.clvMin != -1 && - vbi.clvSec != -1 && vbi.clvPicNo != -1) clvCount++; - } - qDebug() << "TbcSources::getIsSourceCav(): Got" << cavCount << "CAV picture codes and" << clvCount << "CLV timecodes"; - - // If the metadata has no picture numbers or time-codes, we cannot use the source - if (cavCount == 0 && clvCount == 0) { - qDebug() << "TbcSources::getIsSourceCav(): Source does not seem to contain valid CAV picture numbers or CLV time-codes - cannot process"; - return false; - } - - // Determine disc type - if (cavCount > clvCount) { - sourceVideos[sourceNumber]->isSourceCav = true; - qDebug() << "TbcSources::getIsSourceCav(): Got" << cavCount << "valid CAV picture numbers - source disc type is CAV"; - qInfo() << "Disc type is CAV"; - } else { - sourceVideos[sourceNumber]->isSourceCav = false; - qDebug() << "TbcSources::getIsSourceCav(): Got" << clvCount << "valid CLV picture numbers - source disc type is CLV"; - qInfo() << "Disc type is CLV"; - - } - - // Disc has been mapped, so we can use the first and last frame numbers as the - // min and max range of VBI frame numbers in the input source - QVector vbi1 = sourceVideos[sourceNumber]->ldDecodeMetaData.getFieldVbi(sourceVideos[sourceNumber]-> - ldDecodeMetaData.getFirstFieldNumber(1)).vbiData; - QVector vbi2 = sourceVideos[sourceNumber]->ldDecodeMetaData.getFieldVbi(sourceVideos[sourceNumber]-> - ldDecodeMetaData.getSecondFieldNumber(1)).vbiData; - VbiDecoder::Vbi vbi = vbiDecoder.decodeFrame(vbi1[0], vbi1[1], vbi1[2], vbi2[0], vbi2[1], vbi2[2]); - - if (sourceVideos[sourceNumber]->isSourceCav) { - sourceVideos[sourceNumber]->minimumVbiFrameNumber = vbi.picNo; - } else { - LdDecodeMetaData::ClvTimecode timecode; - timecode.hours = vbi.clvHr; - timecode.minutes = vbi.clvMin; - timecode.seconds = vbi.clvSec; - timecode.pictureNumber = vbi.clvPicNo; - sourceVideos[sourceNumber]->minimumVbiFrameNumber = sourceVideos[sourceNumber]->ldDecodeMetaData.convertClvTimecodeToFrameNumber(timecode); - } - - vbi1 = sourceVideos[sourceNumber]->ldDecodeMetaData.getFieldVbi(sourceVideos[sourceNumber]-> - ldDecodeMetaData.getFirstFieldNumber(sourceVideos[sourceNumber]->ldDecodeMetaData.getNumberOfFrames())).vbiData; - vbi2 = sourceVideos[sourceNumber]->ldDecodeMetaData.getFieldVbi(sourceVideos[sourceNumber]-> - ldDecodeMetaData.getSecondFieldNumber(sourceVideos[sourceNumber]->ldDecodeMetaData.getNumberOfFrames())).vbiData; - vbi = vbiDecoder.decodeFrame(vbi1[0], vbi1[1], vbi1[2], vbi2[0], vbi2[1], vbi2[2]); - - if (sourceVideos[sourceNumber]->isSourceCav) { - sourceVideos[sourceNumber]->maximumVbiFrameNumber = vbi.picNo; - } else { - LdDecodeMetaData::ClvTimecode timecode; - timecode.hours = vbi.clvHr; - timecode.minutes = vbi.clvMin; - timecode.seconds = vbi.clvSec; - timecode.pictureNumber = vbi.clvPicNo; - sourceVideos[sourceNumber]->maximumVbiFrameNumber = sourceVideos[sourceNumber]->ldDecodeMetaData.convertClvTimecodeToFrameNumber(timecode); - } - - if (sourceVideos[sourceNumber]->isSourceCav) { - // If the source is CAV frame numbering should be a minimum of 1 (it - // can be 0 for CLV sources) - if (sourceVideos[sourceNumber]->minimumVbiFrameNumber < 1) { - qCritical() << "CAV start frame of" << sourceVideos[sourceNumber]->minimumVbiFrameNumber << "is out of bounds (should be 1 or above)"; - return false; - } - } - - qInfo() << "VBI frame number range is" << sourceVideos[sourceNumber]->minimumVbiFrameNumber << "to" << - sourceVideos[sourceNumber]->maximumVbiFrameNumber; - - return true; -} - -// Method to convert a VBI frame number to a sequential frame number -qint32 TbcSources::convertVbiFrameNumberToSequential(qint32 vbiFrameNumber, qint32 sourceNumber) -{ - // Offset the VBI frame number to get the sequential source frame number - return vbiFrameNumber - sourceVideos[sourceNumber]->minimumVbiFrameNumber + 1; -} - - - - - - - - - - - - - - - - - diff --git a/tools/ld-diffdod/tbcsources.h b/tools/ld-diffdod/tbcsources.h deleted file mode 100644 index 1cda62765..000000000 --- a/tools/ld-diffdod/tbcsources.h +++ /dev/null @@ -1,85 +0,0 @@ -/************************************************************************ - - tbcsources.h - - ld-diffdod - TBC Differential Drop-Out Detection tool - Copyright (C) 2019 Simon Inns - - This file is part of ld-decode-tools. - - ld-diffdod is free software: you can redistribute it and/or - modify it under the terms of the GNU General Public License as - published by the Free Software Foundation, either version 3 of the - License, or (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -************************************************************************/ - -#ifndef TBCSOURCES_H -#define TBCSOURCES_H - -#include -#include -#include -#include - -// TBC library includes -#include "sourcevideo.h" -#include "lddecodemetadata.h" -#include "vbidecoder.h" -#include "filters.h" - -class TbcSources : public QObject -{ - Q_OBJECT -public: - explicit TbcSources(QObject *parent = nullptr); - - bool loadSource(QString filename, bool reverse); - bool unloadSource(); - bool saveSources(qint32 vbiStartFrame, qint32 length, qint32 dodThreshold, bool lumaClip); - - qint32 getNumberOfAvailableSources(); - qint32 getMinimumVbiFrameNumber(); - qint32 getMaximumVbiFrameNumber(); - void verifySources(qint32 vbiStartFrame, qint32 length); - -private: - // Source definition - struct Source { - SourceVideo sourceVideo; - LdDecodeMetaData ldDecodeMetaData; - QString filename; - qint32 minimumVbiFrameNumber; - qint32 maximumVbiFrameNumber; - bool isSourceCav; - }; - - // The frame number is common between sources - qint32 currentVbiFrameNumber; - - QVector sourceVideos; - qint32 currentSource; - - void performFrameDiffDod(qint32 targetVbiFrame, qint32 dodOnThreshold, bool lumaClip); - QVector getFieldData(qint32 targetVbiFrame, bool isFirstField); - QVector > getFieldDiff(QVector &fields, qint32 dodThreshold); - void performLumaClip(QVector &fields, QVector> &fieldsDiff); - QVector getFieldDropouts(QVector > &fieldsDiff); - void writeDropoutMetadata(QVector &firstFieldDropouts, - QVector &secondFieldDropouts, qint32 targetVbiFrame); - void concatenateFieldDropouts(QVector &dropouts); - - QVector getAvailableSourcesForFrame(qint32 vbiFrameNumber); - bool setDiscTypeAndMaxMinFrameVbi(qint32 sourceNumber); - qint32 convertVbiFrameNumberToSequential(qint32 vbiFrameNumber, qint32 sourceNumber); -}; - -#endif // TBCSOURCES_H diff --git a/tools/ld-dropout-correct/main.cpp b/tools/ld-dropout-correct/main.cpp index 31463c88d..8a13821f6 100644 --- a/tools/ld-dropout-correct/main.cpp +++ b/tools/ld-dropout-correct/main.cpp @@ -188,6 +188,16 @@ int main(int argc, char *argv[]) } } + // Check that the output file does not already exist + if (outputFilename != "-") { + QFileInfo outputFileInfo(outputFilename); + if (outputFileInfo.exists()) { + // Quit with error + qCritical("Specified output file already exists - will not overwrite"); + return -1; + } + } + // Metadata filename for output TBC QString outputJsonFilename = outputFilename + ".json"; if (parser.isSet(outputJsonOption)) { diff --git a/tools/library/tbc/filters.cpp b/tools/library/tbc/filters.cpp index 741bfb847..9f6dad312 100644 --- a/tools/library/tbc/filters.cpp +++ b/tools/library/tbc/filters.cpp @@ -29,25 +29,21 @@ #include // PAL - Filter at Fsc/2 (Fsc = 4433618 (/2 = 2,216,809), sample rate = 17,734,472) -// 2.2 MHz LPF - 15 Taps +// 2.2 MHz LPF - 5 Taps // import scipy.signal -// scipy.signal.firwin(15, [2.2e6/17734472], window='hamming') -static constexpr std::array palLumaFilterCoeffs { - 0.00188029, 0.00616523, 0.01927009, 0.04479076, 0.08068761, - 0.11896474, 0.14846274, 0.15955709, 0.14846274, 0.11896474, - 0.08068761, 0.04479076, 0.01927009, 0.00616523, 0.00188029 +// scipy.signal.firwin(5, [2.2e6/17734472], window='hamming') +static constexpr std::array palLumaFilterCoeffs { + 0.03283437, 0.23959832, 0.45513461, 0.23959832, 0.03283437 }; static constexpr auto palLumaFilter = makeFIRFilter(palLumaFilterCoeffs); // NTSC - Filter at Fsc/2 (Fsc = 3579545 (/2 = 1,789,772.5), sample rate = 14,318,180) -// 1.8 MHz LPF - 15 Taps +// 1.8 MHz LPF - 5 Taps // import scipy.signal -// scipy.signal.firwin(15, [1.8e6/14318180], window='hamming') -static constexpr std::array ntscLumaFilterCoeffs { - 0.00170818, 0.00592632, 0.01890583, 0.04442077, 0.0805412 , - 0.11921885, 0.14910167, 0.16035437, 0.14910167, 0.11921885, - 0.0805412 , 0.04442077, 0.01890583, 0.00592632, 0.00170818 +// scipy.signal.firwin(5, [1.8e6/14318180], window='hamming') +static constexpr std::array ntscLumaFilterCoeffs { + 0.03275786, 0.23955702, 0.45537024, 0.23955702, 0.03275786 }; static constexpr auto ntscLumaFilter = makeFIRFilter(ntscLumaFilterCoeffs);