#417 Filter-Chains

main
Maveno 11 months ago
parent 49c1164b8b
commit 1caf4beb72

@ -19,7 +19,6 @@ from ffx.show_descriptor import ShowDescriptor
from ffx.track_type import TrackType
from ffx.video_encoder import VideoEncoder
from ffx.track_disposition import TrackDisposition
from ffx.nlmeans_controller import NlmeansController
from ffx.process import executeProcess
from ffx.helper import filterFilename
@ -27,6 +26,12 @@ from ffx.helper import filterFilename
from ffx.constants import DEFAULT_QUALITY, DEFAULT_AV1_PRESET
from ffx.constants import DEFAULT_STEREO_BANDWIDTH, DEFAULT_AC3_BANDWIDTH, DEFAULT_DTS_BANDWIDTH, DEFAULT_7_1_BANDWIDTH
from ffx.filter.quality_filter import QualityFilter
from ffx.filter.preset_filter import PresetFilter
from ffx.filter.nlmeans_filter import NlmeansFilter
VERSION='0.2.2'
@ -277,8 +282,8 @@ def checkUniqueDispositions(context, mediaDescriptor: MediaDescriptor):
@click.option('-v', '--video-encoder', type=str, default=FfxController.DEFAULT_VIDEO_ENCODER, help=f"Target video encoder (vp9 or av1)", show_default=True)
@click.option('-q', '--quality', type=str, default=DEFAULT_QUALITY, help=f"Quality settings to be used with VP9 encoder", show_default=True)
@click.option('-p', '--preset', type=str, default=DEFAULT_AV1_PRESET, help=f"Quality preset to be used with AV1 encoder", show_default=True)
@click.option('-q', '--quality', type=str, default="", help=f"Quality settings to be used with VP9 encoder")
@click.option('-p', '--preset', type=str, default="", help=f"Quality preset to be used with AV1 encoder")
@click.option('-a', '--stereo-bitrate', type=int, default=DEFAULT_STEREO_BANDWIDTH, help=f"Bitrate in kbit/s to be used to encode stereo audio streams", show_default=True)
@click.option('--ac3', type=int, default=DEFAULT_AC3_BANDWIDTH, help=f"Bitrate in kbit/s to be used to encode 5.1 audio streams", show_default=True)
@ -409,14 +414,6 @@ def convert(ctx,
context['resource_limits']['cpu_percent'] = cpu
context['denoiser'] = NlmeansController(parameters = denoise,
strength = denoise_strength,
patchSize = denoise_patch_size,
chromaPatchSize = denoise_chroma_patch_size,
researchWindow = denoise_research_window,
chromaResearchWindow = denoise_chroma_research_window,
useHardware = denoise_use_hw)
context['import_subtitles'] = (subtitle_directory and subtitle_prefix)
if context['import_subtitles']:
context['subtitle_directory'] = subtitle_directory
@ -494,9 +491,13 @@ def convert(ctx,
qualityTokens = quality.split(',')
q_list = [q for q in qualityTokens if q.isnumeric()]
ctx.obj['logger'].debug(f"Qualities: {q_list}")
presetTokens = preset.split(',')
p_list = [p for p in presetTokens if p.isnumeric()]
ctx.obj['logger'].debug(f"Presets: {p_list}")
context['bitrates'] = {}
context['bitrates']['stereo'] = str(stereo_bitrate) if str(stereo_bitrate).endswith('k') else f"{stereo_bitrate}k"
context['bitrates']['ac3'] = str(ac3) if str(ac3).endswith('k') else f"{ac3}k"
@ -519,8 +520,29 @@ def convert(ctx,
tc = TmdbController() if context['use_tmdb'] else None
qualityKwargs = {QualityFilter.QUALITY_KEY: quality}
qf = QualityFilter(**qualityKwargs)
presetKwargs = {PresetFilter.PRESET_KEY: preset}
PresetFilter(**presetKwargs)
denoiseKwargs = {}
if denoise_strength:
denoiseKwargs[NlmeansFilter.STRENGTH_KEY] = denoise_strength
if denoise_patch_size:
denoiseKwargs[NlmeansFilter.PATCH_SIZE_KEY] = denoise_patch_size
if denoise_chroma_patch_size:
denoiseKwargs[NlmeansFilter.CHROMA_PATCH_SIZE_KEY] = denoise_chroma_patch_size
if denoise_research_window:
denoiseKwargs[NlmeansFilter.RESEARCH_WINDOW_KEY] = denoise_research_window
if denoise_chroma_research_window:
denoiseKwargs[NlmeansFilter.CHROMA_RESEARCH_WINDOW_KEY] = denoise_chroma_research_window
if denoise != 'none' or denoiseKwargs:
NlmeansFilter(**denoiseKwargs)
ctx.obj['logger'].info(f"\nRunning {len(existingSourcePaths) * len(q_list)} jobs")
chainYield = list(qf.getChainYield())
ctx.obj['logger'].info(f"\nRunning {len(existingSourcePaths) * len(chainYield)} jobs")
jobIndex = 0
@ -650,18 +672,24 @@ def convert(ctx,
if 'se' in targetSuffices.keys():
del targetSuffices['se']
ctx.obj['logger'].debug(f"fileBasename={sourceFileBasename}")
for q in q_list:
for chainIteration in chainYield:
ctx.obj['logger'].debug(f"\nchain iteration: {chainIteration}\n")
if len(q_list) > 1:
targetSuffices['q'] = f"q{q}"
# if len(q_list) > 1:
# targetSuffices['q'] = f"q{q}"
ctx.obj['logger'].debug(f"\nRunning job {jobIndex} file={sourcePath} q={q}")
chainVariant = '-'.join([fy['variant'] for fy in chainIteration])
ctx.obj['logger'].debug(f"\nRunning job {jobIndex} file={sourcePath} variant={chainVariant}")
jobIndex += 1
continue
ctx.obj['logger'].debug(f"label={label if label else 'Falsy'}")
ctx.obj['logger'].debug(f"sourceFileBasename={sourceFileBasename}")
@ -676,9 +704,15 @@ def convert(ctx,
if 'se' in targetSuffices.keys():
targetFilenameTokens += [targetSuffices['se']]
if 'q' in targetSuffices.keys():
targetFilenameTokens += [targetSuffices['q']]
# if 'q' in targetSuffices.keys():
# targetFilenameTokens += [targetSuffices['q']]
for filterYield in chainIteration:
# filterIdentifier = filterYield['identifier']
# filterParameters = filterYield['parameters']
# filterSuffices = filterYield['suffices']
targetFilenameTokens += filterYield['suffices']
#TODO #387
# targetFilename = ((f"{sourceFileBasename}_q{q}" if len(q_list) > 1 else sourceFileBasename)
@ -695,8 +729,7 @@ def convert(ctx,
targetPath,
targetFormat,
context['video_encoder'],
q,
preset)
chainIteration)
#TODO: click.confirm('Warning! This file is not compliant to the defined source schema! Do you want to continue?', abort=True)

@ -13,6 +13,10 @@ from ffx.track_disposition import TrackDisposition
from ffx.constants import DEFAULT_QUALITY, DEFAULT_AV1_PRESET
from ffx.constants import DEFAULT_CROP_START, DEFAULT_CROP_LENGTH
from ffx.filter.quality_filter import QualityFilter
from ffx.filter.preset_filter import PresetFilter
from ffx.filter.nlmeans_filter import NlmeansFilter
class FfxController():
@ -44,8 +48,8 @@ class FfxController():
self.__configurationData = self.__context['config'].getData()
# Convenience
self.__niceness = self.__context['resource_limits']['niceness'] if 'resource_limits' in self.__context.keys() and 'niceness' in self.__context['resource_limits'].keys() else 99
self.__cpuPercent = self.__context['resource_limits']['cpu_percent'] if 'resource_limits' in self.__context.keys() and 'cpu_percent' in self.__context['resource_limits'].keys() else 0
# self.__niceness = self.__context['resource_limits']['niceness'] if 'resource_limits' in self.__context.keys() and 'niceness' in self.__context['resource_limits'].keys() else 99
# self.__cpuPercent = self.__context['resource_limits']['cpu_percent'] if 'resource_limits' in self.__context.keys() and 'cpu_percent' in self.__context['resource_limits'].keys() else 0
self.__logger = context['logger']
@ -242,8 +246,19 @@ class FfxController():
targetPath,
targetFormat: str = '',
videoEncoder: VideoEncoder = VideoEncoder.VP9,
quality: int = DEFAULT_QUALITY,
preset: int = DEFAULT_AV1_PRESET):
chainIteration: list = []):
# quality: int = DEFAULT_QUALITY,
# preset: int = DEFAULT_AV1_PRESET):
qualityFilters = [fy for fy in chainIteration if fy['identifier'] == 'quality']
presetFilters = [fy for fy in chainIteration if fy['identifier'] == 'preset']
denoiseFilters = [fy for fy in chainIteration if fy['identifier'] == 'nlmeans']
quality = qualityFilters[0]['parameters']['quality'] if qualityFilters else QualityFilter.DEFAULT_QUALITY
preset = presetFilters[0]['parameters']['preset'] if presetFilters else PresetFilter.DEFAULT_PRESET
denoiseTokens = denoiseFilters[0]['tokens'] if denoiseFilters else []
commandTokens = FfxController.COMMAND_TOKENS + ['-i', sourcePath]
@ -257,7 +272,7 @@ class FfxController():
# Optional tokens
commandSequence += self.generateMetadataTokens()
commandSequence += self.__context['denoiser'].generateDenoiseTokens()
commandSequence += denoiseTokens
commandSequence += (self.generateAudioEncodingTokens()
+ self.generateAV1Tokens(int(quality), int(preset))
@ -309,7 +324,7 @@ class FfxController():
# Optional tokens
commandSequence2 += self.generateMetadataTokens()
commandSequence2 += self.__context['denoiser'].generateDenoiseTokens()
commandSequence2 += denoiseTokens
commandSequence2 += self.generateVP9Pass2Tokens(int(quality)) + self.generateAudioEncodingTokens()

@ -0,0 +1,17 @@
import itertools
class Filter():
filterChain: list = []
def __init__(self, filter):
self.filterChain.append(filter)
def getFilterChain(self):
return self.filterChain
def getChainYield(self):
for fy in itertools.product(*[f.getYield() for f in self.filterChain]):
yield fy

@ -0,0 +1,162 @@
import itertools
from .filter import Filter
class NlmeansFilter(Filter):
IDENTIFIER = 'nlmeans'
DEFAULT_STRENGTH: float = 2.8
DEFAULT_PATCH_SIZE: int = 13
DEFAULT_CHROMA_PATCH_SIZE: int = 9
DEFAULT_RESEARCH_WINDOW: int = 23
DEFAULT_CHROMA_RESEARCH_WINDOW: int= 17
STRENGTH_KEY = 'strength'
PATCH_SIZE_KEY = 'patch_size'
CHROMA_PATCH_SIZE_KEY = 'chroma_patch_size'
RESEARCH_WINDOW_KEY = 'research_window'
CHROMA_RESEARCH_WINDOW_KEY = 'chroma_research_window'
def __init__(self, **kwargs):
self.__useHardware = kwargs.get('use_hardware', False)
self.__strengthList = []
strength = kwargs.get(NlmeansFilter.STRENGTH_KEY, '')
if strength:
strengthTokens = strength.split(',')
for st in strengthTokens:
try:
strengthValue = float(st)
except:
raise ValueError('NlmeansFilter: Strength value has to be of type float')
if strengthValue < 1.0 or strengthValue > 30.0:
raise ValueError('NlmeansFilter: Strength value has to be between 1.0 and 30.0')
self.__strengthList.append(strengthValue)
else:
self.__strengthList = [NlmeansFilter.DEFAULT_STRENGTH]
self.__patchSizeList = []
patchSize = kwargs.get(NlmeansFilter.PATCH_SIZE_KEY, '')
if patchSize:
patchSizeTokens = patchSize.split(',')
for pst in patchSizeTokens:
try:
patchSizeValue = int(pst)
except:
raise ValueError('NlmeansFilter: Patch size value has to be of type int')
if patchSizeValue < 0 or patchSizeValue > 99:
raise ValueError('NlmeansFilter: Patch size value has to be between 0 and 99')
if patchSizeValue % 2 == 0:
raise ValueError('NlmeansFilter: Patch size value has to an odd number')
self.__patchSizeList.append(patchSizeValue)
else:
self.__patchSizeList = [NlmeansFilter.DEFAULT_PATCH_SIZE]
self.__chromaPatchSizeList = []
chromaPatchSize = kwargs.get(NlmeansFilter.CHROMA_PATCH_SIZE_KEY, '')
if chromaPatchSize:
chromaPatchSizeTokens = chromaPatchSize.split(',')
for cpst in chromaPatchSizeTokens:
try:
chromaPatchSizeValue = int(pst)
except:
raise ValueError('NlmeansFilter: Chroma patch size value has to be of type int')
if chromaPatchSizeValue < 0 or chromaPatchSizeValue > 99:
raise ValueError('NlmeansFilter: Chroma patch value has to be between 0 and 99')
if chromaPatchSizeValue % 2 == 0:
raise ValueError('NlmeansFilter: Chroma patch value has to an odd number')
self.__chromaPatchSizeList.append(chromaPatchSizeValue)
else:
self.__chromaPatchSizeList = [NlmeansFilter.DEFAULT_CHROMA_PATCH_SIZE]
self.__researchWindowList = []
researchWindow = kwargs.get(NlmeansFilter.RESEARCH_WINDOW_KEY, '')
if researchWindow:
researchWindowTokens = researchWindow.split(',')
for rwt in researchWindowTokens:
try:
researchWindowValue = int(rwt)
except:
raise ValueError('NlmeansFilter: Research window value has to be of type int')
if researchWindowValue < 0 or researchWindowValue > 99:
raise ValueError('NlmeansFilter: Research window value has to be between 0 and 99')
if researchWindowValue % 2 == 0:
raise ValueError('NlmeansFilter: Research window value has to an odd number')
self.__researchWindowList.append(researchWindowValue)
else:
self.__researchWindowList = [NlmeansFilter.DEFAULT_RESEARCH_WINDOW]
self.__chromaResearchWindowList = []
chromaResearchWindow = kwargs.get(NlmeansFilter.CHROMA_RESEARCH_WINDOW_KEY, '')
if chromaResearchWindow:
chromaResearchWindowTokens = chromaResearchWindow.split(',')
for crwt in chromaResearchWindowTokens:
try:
chromaResearchWindowValue = int(crwt)
except:
raise ValueError('NlmeansFilter: Chroma research window value has to be of type int')
if chromaResearchWindowValue < 0 or chromaResearchWindowValue > 99:
raise ValueError('NlmeansFilter: Chroma research window value has to be between 0 and 99')
if chromaResearchWindowValue % 2 == 0:
raise ValueError('NlmeansFilter: Chroma research window value has to an odd number')
self.__chromaResearchWindowList.append(chromaResearchWindowValue)
else:
self.__chromaResearchWindowList = [NlmeansFilter.DEFAULT_CHROMA_RESEARCH_WINDOW]
super().__init__(self)
def getPayload(self, iteration):
strength = iteration[0]
patchSize = iteration[1]
chromaPatchSize = iteration[2]
researchWindow = iteration[3]
chromaResearchWindow = iteration[4]
suffices = []
if len(self.__strengthList) > 1:
suffices += [f"ds{strength}"]
if len(self.__patchSizeList) > 1:
suffices += [f"dp{patchSize}"]
if len(self.__chromaPatchSizeList) > 1:
suffices += [f"dpc{chromaPatchSize}"]
if len(self.__researchWindowList) > 1:
suffices += [f"dr{researchWindow}"]
if len(self.__chromaResearchWindowList) > 1:
suffices += [f"drc{chromaResearchWindow}"]
filterName = 'nlmeans_opencl' if self.__useHardware else 'nlmeans'
payload = {'identifier': NlmeansFilter.IDENTIFIER,
'parameters': {
'strength': strength,
'patch_size': patchSize,
'chroma_patch_size': chromaPatchSize,
'research_window': researchWindow,
'chroma_research_window': chromaResearchWindow
},
'suffices': suffices,
'variant': f"DS{strength}-DP{patchSize}-DPC{chromaPatchSize}"
+ f"-DR{researchWindow}-DRC{chromaResearchWindow}",
'tokens': ['-vf', f"{filterName}=s={strength}"
+ f":p={patchSize}"
+ f":pc={chromaPatchSize}"
+ f":r={researchWindow}"
+ f":rc={chromaResearchWindow}"]}
return payload
def getYield(self):
for it in itertools.product(self.__strengthList,
self.__patchSizeList,
self.__chromaPatchSizeList,
self.__researchWindowList,
self.__chromaResearchWindowList):
yield self.getPayload(it)

@ -0,0 +1,54 @@
import itertools
from .filter import Filter
class PresetFilter(Filter):
IDENTIFIER = 'preset'
DEFAULT_PRESET = 5
PRESET_KEY = 'preset'
def __init__(self, **kwargs):
self.__presetsList = []
presets = str(kwargs.get(PresetFilter.PRESET_KEY, ''))
if presets:
presetTokens = presets.split(',')
for q in presetTokens:
try:
presetValue = int(q)
except:
raise ValueError('PresetFilter: Preset value has to be of type int')
if presetValue < 0 or presetValue > 13:
raise ValueError('PresetFilter: Preset value has to be between 0 and 13')
self.__presetsList.append(presetValue)
else:
self.__presetsList = [PresetFilter.DEFAULT_PRESET]
super().__init__(self)
def getPayload(self, preset):
suffices = []
if len(self.__presetsList) > 1:
suffices += [f"p{preset}"]
payload = {'identifier': PresetFilter.IDENTIFIER,
'parameters': {
'preset': preset
},
'suffices': suffices,
'variant': f"P{preset}",
'tokens': []}
return payload
def getYield(self):
for q in self.__presetsList:
yield self.getPayload(q)

@ -0,0 +1,54 @@
import itertools
from .filter import Filter
class QualityFilter(Filter):
IDENTIFIER = 'quality'
DEFAULT_QUALITY = 32
QUALITY_KEY = 'quality'
def __init__(self, **kwargs):
self.__qualitiesList = []
qualities = kwargs.get(QualityFilter.QUALITY_KEY, '')
if qualities:
qualityTokens = qualities.split(',')
for q in qualityTokens:
try:
qualityValue = int(q)
except:
raise ValueError('QualityFilter: Quality value has to be of type int')
if qualityValue < 0 or qualityValue > 63:
raise ValueError('QualityFilter: Quality value has to be between 0 and 63')
self.__qualitiesList.append(qualityValue)
else:
self.__qualitiesList = [QualityFilter.DEFAULT_QUALITY]
super().__init__(self)
def getPayload(self, quality):
suffices = []
if len(self.__qualitiesList) > 1:
suffices += [f"q{quality}"]
payload = {'identifier': QualityFilter.IDENTIFIER,
'parameters': {
'quality': quality
},
'suffices': suffices,
'variant': f"Q{quality}",
'tokens': []}
return payload
def getYield(self):
for q in self.__qualitiesList:
yield self.getPayload(q)

@ -0,0 +1,6 @@
from .filter import Filter
class ScaleFilter(Filter):
def __init__(self):
super().__init__(self)

@ -74,5 +74,6 @@ def filterFilename(fileName: str) -> str:
fileName = str(fileName).replace(':', ';')
fileName = str(fileName).replace('*', '')
fileName = str(fileName).replace("'", '')
fileName = str(fileName).replace("?", '#')
return fileName.strip()

@ -1,142 +0,0 @@
class NlmeansController():
"""
s: double
Denoising strength (from 1 to 30) (default 1)
Trade-off between noise removal and detail retention. Comparable to gaussian sigma.
p: int patch size (from 0 to 99) (default 7)
Catches larger areas reducing broader noise patterns, but costly
pc: int patch size for chroma planes (from 0 to 99) (default 0)
r: int research window (from 0 to 99) (default 15)
Range to search for comparable patches.
Better filtering but costly
rc: int research window for chroma planes (from 0 to 99) (default 0)
Good values to denoise film grain that was subobtimally encoded:
strength: float = 2.8
patchSize: int = 12
chromaPatchSize: int = 8
researchWindow: int = 22
chromaResearchWindow: int= 16
"""
DEFAULT_STRENGTH: float = 2.8
DEFAULT_PATCH_SIZE: int = 13
DEFAULT_CHROMA_PATCH_SIZE: int = 9
DEFAULT_RESEARCH_WINDOW: int = 23
DEFAULT_CHROMA_RESEARCH_WINDOW: int= 17
def __init__(self,
parameters: str = "none",
strength: str = "",
patchSize: str = "",
chromaPatchSize: str = "",
researchWindow: str = "",
chromaResearchWindow: str = "",
useHardware: bool = False):
self.__isActive = (parameters != "none"
or strength
or patchSize
or chromaPatchSize
or researchWindow
or chromaResearchWindow)
self.__useHardware = useHardware
parameterTokens = parameters.split(',')
self.__strengthList = []
if strength:
strengthTokens = strength.split(',')
for st in strengthTokens:
try:
strengthValue = float(st)
except:
raise ValueError('NlmeansController: Strength value has to be of type float')
if strengthValue < 1.0 or strengthValue > 30.0:
raise ValueError('NlmeansController: Strength value has to be between 1.0 and 30.0')
self.__strengthList.append(strengthValue)
else:
self.__strengthList = [NlmeansController.DEFAULT_STRENGTH]
self.__patchSizeList = []
if patchSize:
patchSizeTokens = patchSize.split(',')
for pst in patchSizeTokens:
try:
patchSizeValue = int(pst)
except:
raise ValueError('NlmeansController: Patch size value has to be of type int')
if patchSizeValue < 0 or patchSizeValue > 99:
raise ValueError('NlmeansController: Patch size value has to be between 0 and 99')
if patchSizeValue % 2 == 0:
raise ValueError('NlmeansController: Patch size value has to an odd number')
self.__patchSizeList.append(patchSizeValue)
else:
self.__patchSizeList = [NlmeansController.DEFAULT_PATCH_SIZE]
self.__chromaPatchSizeList = []
if chromaPatchSize:
chromaPatchSizeTokens = chromaPatchSize.split(',')
for cpst in chromaPatchSizeTokens:
try:
chromaPatchSizeValue = int(pst)
except:
raise ValueError('NlmeansController: Chroma patch size value has to be of type int')
if chromaPatchSizeValue < 0 or chromaPatchSizeValue > 99:
raise ValueError('NlmeansController: Chroma patch value has to be between 0 and 99')
if chromaPatchSizeValue % 2 == 0:
raise ValueError('NlmeansController: Chroma patch value has to an odd number')
self.__chromaPatchSizeList.append(chromaPatchSizeValue)
else:
self.__chromaPatchSizeList = [NlmeansController.DEFAULT_CHROMA_PATCH_SIZE]
self.__researchWindowList = []
if researchWindow:
researchWindowTokens = researchWindow.split(',')
for rwt in researchWindowTokens:
try:
researchWindowValue = int(rwt)
except:
raise ValueError('NlmeansController: Research window value has to be of type int')
if researchWindowValue < 0 or researchWindowValue > 99:
raise ValueError('NlmeansController: Research window value has to be between 0 and 99')
if researchWindowValue % 2 == 0:
raise ValueError('NlmeansController: Research window value has to an odd number')
self.__researchWindowList.append(researchWindowValue)
else:
self.__researchWindowList = [NlmeansController.DEFAULT_RESEARCH_WINDOW]
self.__chromaResearchWindowList = []
if chromaResearchWindow:
chromaResearchWindowTokens = chromaResearchWindow.split(',')
for crwt in chromaResearchWindowTokens:
try:
chromaResearchWindowValue = int(crwt)
except:
raise ValueError('NlmeansController: Chroma research window value has to be of type int')
if chromaResearchWindowValue < 0 or chromaResearchWindowValue > 99:
raise ValueError('NlmeansController: Chroma research window value has to be between 0 and 99')
if chromaResearchWindowValue % 2 == 0:
raise ValueError('NlmeansController: Chroma research window value has to an odd number')
self.__chromaResearchWindowList.append(chromaResearchWindowValue)
else:
self.__chromaResearchWindowList = [NlmeansController.DEFAULT_CHROMA_RESEARCH_WINDOW]
def isActive(self):
return self.__isActive
def generateDenoiseTokens(self):
filterName = 'nlmeans_opencl' if self.__useHardware else 'nlmeans'
return ['-vf', f"{filterName}=s={self.__strengthList[0]}"
+ f":p={self.__patchSizeList[0]}"
+ f":pc={self.__chromaPatchSizeList[0]}"
+ f":r={self.__researchWindowList[0]}"
+ f":rc={self.__chromaResearchWindowList[0]}"] if self.__isActive else []

@ -0,0 +1,9 @@
from ffx.filter.nlmeans_filter import NlmeansFilter
from ffx.filter.quality_filter import QualityFilter
q = QualityFilter()
#q = QualityFilter('32,34')
# NlmeansFilter(researchWindow='5,7,9', strength='2.0,3.0,4.0')
for cy in q.getChainYield():
print(cy)
Loading…
Cancel
Save