diff --git a/Admin/rtbPublishReferenceData.m b/Admin/rtbPublishReferenceData.m new file mode 100644 index 0000000000000000000000000000000000000000..7b376d0eb731c219e5398a195769129225b6e50d --- /dev/null +++ b/Admin/rtbPublishReferenceData.m @@ -0,0 +1,92 @@ +function artifacts = rtbPublishReferenceData(varargin) +% Use RemoteDataToolbox to publish reference data to brainard-archiva. +% +% Archiva server "brainard-archiva" on AWS at http://52.32.77.154/ +% and repository called RenderToolbox. +% see rdt-config-render-toolbox.json +% +% Reference data on Amazon S3 at +% s3://render-toolbox-reference/all-example-scenes/2016-10-26-21-24-21 +% +% Use yas3fs to mount the bucket before running this script. Then this +% script can use the file system instead of the S3 API. +% +% Should run this from an AWS instance under the Brainard Lab AWS accout. +% This will avoid data transfer to the local workstation, which will make +% it go a lot faster and cheaper! +% +% For each example scene, make a zip archive. Publish at a path like +% reference-renderings/rtbMakeDragon +% +% Use the "epic scene test" date as the version, like +% 2016-10-26-21-24-21 +% + +parser = inputParser(); +parser.addParameter('rdtConfig', 'render-toolbox'); +parser.addParameter('referenceRoot', pwd(), @ischar); +parser.addParameter('tempRoot', fullfile(tempdir(), 'rtbPublishReferenceData'), @ischar); +parser.addParameter('referenceVersion', 'test', @ischar); +parser.addParameter('remotePath', 'reference-data', @ischar); +parser.addParameter('deployToolboxes', true, @islogical); +parser.addParameter('dryRun', true, @islogical); +parser.parse(varargin{:}); +rdtConfig = parser.Results.rdtConfig; +referenceRoot = parser.Results.referenceRoot; +tempRoot = parser.Results.tempRoot; +referenceVersion = parser.Results.referenceVersion; +remotePath = parser.Results.remotePath; +deployToolboxes = parser.Results.deployToolboxes; +dryRun = parser.Results.dryRun; + +if deployToolboxes + tbUse({'RenderToolbox4', 'RemoteDataToolbox'}); +end + +if 7 ~= exist(tempRoot, 'dir') + mkdir(tempRoot); +end + +% iterate subfolders of referenceRoot for example names +[exampleNames, nExamples] = subfolderNames(referenceRoot); +artifactCell = cell(1, nExamples); +for ee = 1:nExamples + exampleName = exampleNames{ee}; + exampleDir = fullfile(referenceRoot, exampleName); + + % zip up the example + archiveTemp = fullfile(tempRoot, [exampleName '.zip']); + if ~dryRun + zip(archiveTemp, '.', exampleDir); + end + + % publish the zip + artifactCell{ee} = publishFile(rdtConfig, archiveTemp, remotePath,... + exampleName, referenceVersion, dryRun); +end +artifacts = [artifactCell{:}]; + +function [names, nNames] = subfolderNames(parentPath) +parentDir = dir(parentPath); +parentDir = parentDir(3:end); +names = {parentDir([parentDir.isdir]).name}; +nNames = numel(names); + + +function artifact = publishFile(rdtConfig, fileName, remotePath, exampleName, versionName, dryRun) + +% describe the example +artifactPath = fullfile(remotePath, exampleName); +description = sprintf('version <%s> example <%s>', versionName, exampleName); +disp(description); + +if dryRun + artifact = []; + return; +end + +% go ahead and publish the dir +artifact = rdtPublishArtifact(rdtConfig, fileName, artifactPath, ... + 'artifactId', exampleName, ... + 'version', versionName, ... + 'rescan', true); diff --git a/BatchRenderer/AssimpStrategy/BasicMappings/rtbResourcePath.m b/BatchRenderer/AssimpStrategy/BasicMappings/rtbResourcePath.m deleted file mode 100644 index 7e9403cc5f4e39956f820a594b8625a4c8ca9115..0000000000000000000000000000000000000000 --- a/BatchRenderer/AssimpStrategy/BasicMappings/rtbResourcePath.m +++ /dev/null @@ -1,206 +0,0 @@ -function [outName, isLocated, info] = rtbResourcePath(inName, varargin) -%% Build a reference to a resource file mentioned in a scene. -% -% The idea here is to resolve file references and clean up those -% reference so that they point to existing local files. This is -% useful because "wild" scenes from the web often contain: -% - renferences to non-existent files -% - absolute file paths from another computer -% - truncated file names -% - file names with the wrong CASE -% - file names with non-ASCII characters -% - etc. -% These are all a pain in the neck, but many of these can be resolved -% automatically. -% -% [outName, info] = rtbResourcePath(inName) attempts to do fuzzy -% matching between the given fileName and the files found in pwd(). -% When a match is found, the fileName will be updated. -% -% rtbResourcePath( ... 'strictMatching', strictMatching) whether to perform -% exact file name matching (true) or fuzzy matching, which is more -% permissive and less accurate (false). The default is false, do -% permissive fuzzy matching. -% -% rtbResourcePath( ... 'resourceFolder', resourceFolder) specifies -% a folder to search for local existing files. The default is pwd(). -% -% rtbResourcePath( ... 'writeFullPaths', writeFullPaths) choose -% whether to update the given fileName with a full absolute paths (true), -% or to write just file names without any leading path (false). The -% default is true, write full, absolute paths. -% -% rtbResourcePath( ... 'relativePath', relativePath) specify -% a prefix to add to updated file name, when writeFullPaths is false. -% This is useful in case the located resource file is in a subfolder -% relative to a scene file. The default is '', don't prepend any relative -% path. -% -% rtbResourcePath( ... 'toReplace', toReplace) -% specifies a string containing characters to be replaced in resource file -% names. When found, these characters will be replaced with "_" -% underscores. This is useful to prevent Assimp from transcoding non-ASCII -% characters. For example, Assimp transcodes "-" as "%2d", which is good -% for UTF-8, but breaks some downstream programs like PBRT and Mitsuba. -% The default is '-:', replace hyphens and colons with underscores. -% -% rtbResourcePath( ... 'copyOnReplace', copyOnReplace) choose -% whether to copy files when their names contain replaced characters (true) -% or not (false). This is useful so that renamed resources will point to -% existing files. The default is true, make new copies or resource files -% as their names are replaced. -% -% rtbResourcePath( ... 'useMatlabPath', useMatlabPath) specifies whether -% to search the Matlab path for resource files, after searching the given -% resourceFolder. The default is true, do search the Matlab path. -% -% Returns the given fileName, with modifications. Also returns a logical -% flag, true when the resource was located. Also returns a struct -% of information about what happened. -% -% [outName, isLocated, info] = rtbResourcePath(inName, varargin) -% -% Copyright (c) 2016 mexximp Teame - -parser = inputParser(); -parser.addRequired('inName', @ischar); -parser.addParameter('strictMatching', false, @islogical); -parser.addParameter('resourceFolder', pwd(), @ischar); -parser.addParameter('writeFullPaths', true, @islogical); -parser.addParameter('relativePath', '', @ischar); -parser.addParameter('toReplace', '-:', @ischar); -parser.addParameter('copyOnReplace', true, @islogical); -parser.addParameter('useMatlabPath', true, @islogical); -parser.parse(inName, varargin{:}); -inName = parser.Results.inName; -strictMatching = parser.Results.strictMatching; -resourceFolder = parser.Results.resourceFolder; -writeFullPaths = parser.Results.writeFullPaths; -relativePath = parser.Results.relativePath; -toReplace = parser.Results.toReplace; -copyOnReplace = parser.Results.copyOnReplace; -useMatlabPath = parser.Results.useMatlabPath; - -if strictMatching - matchFunction = @strictMatch; -else - matchFunction = @fuzzyMatch; -end - -isLocated = false; - - -%% Collect files in the resourceFolder. -resourceDir = dir(resourceFolder); -isDir = [resourceDir.isdir]; -resources = {resourceDir(~isDir).name}; - - -%% Try to find a the file in the given resources folder or Matlab path. -resourceMatch = matchResource(inName, resources, matchFunction); -if isempty(resourceMatch) && useMatlabPath && 2 == exist(inName, 'file') - % copy into resources folder and proceed like it was always there - fullPathName = which(inName); - resourceCopy = fullfile(resourceFolder, inName); - copyfile(fullPathName, resourceCopy, 'f'); - resourceMatch = inName; -end - - -%% Did we find it? -if isempty(resourceMatch) - % report an unmatched file - info.verbatimName = inName; - info.writtenName = inName; - info.isMatched = false; - info.matchName = ''; - info.matchFullPath = ''; - outName = inName; - return; -end - - -%% Replace unwanted characters in the file name. -newName = replaceCharacters(resourceMatch, toReplace); -if copyOnReplace && ~isempty(newName) - source = fullfile(resourceFolder, resourceMatch); - destination = fullfile(resourceFolder, newName); - copyfile(source, destination, 'f'); - resourceMatch = newName; -end - - -%% Choose a new file name. -isLocated = true; -resourceFullPath = fullfile(resourceFolder, resourceMatch); -resourceRelativePath = fullfile(relativePath, resourceMatch); -if writeFullPaths - outName = resourceFullPath; -else - outName = resourceRelativePath; -end - - -%% Report a successful update. -info.verbatimName = inName; -info.writtenName = outName; -info.isMatched = true; -info.matchName = resourceMatch; -info.matchFullPath = resourceFullPath; - - -%% Find unwanted characters and replace with underscores. -function newName = replaceCharacters(name, toReplace) -newName = ''; -needsReplacement = false(1, numel(name)); -for ii = 1:numel(toReplace) - needsReplacement = needsReplacement | toReplace(ii) == name; -end -if any(needsReplacement) - newName = name; - newName(needsReplacement) = '_'; -end - - -%% Iterate resources and try to match against a given file. -function resourceMatch = matchResource(fileName, resources, matchFunction) -resourceMatch = ''; -nResources = numel(resources); -for ii = 1:nResources - resource = resources{ii}; - if feval(matchFunction, fileName, resource) - resourceMatch = resource; - return; - end -end - - -%% Fuzzy matching for file names: is b probably a good substitute for a? -% case insensitive -% 4851-nor.jpg matches 4851-normal.jpg -% C:\foo\bar\baz.jpg matches baz.jpg -function isMatch = fuzzyMatch(a, b) -a = lower(a); -b = lower(b); - -[~, aBase, aExt] = fileparts(a); -[~, bBase, bExt] = fileparts(b); - -% one extension is a substring of the other, -% and the base names match -isMatch = (~isempty(strfind(aExt, bExt)) || ~isempty(strfind(bExt, aExt))) ... - && (~isempty(strfind(aBase, bBase)) || ~isempty(strfind(bBase, aBase))); - - -%% Strict matching for file names: b is the same as a, within reason. -% case insensitive -% 4851-nor.jpg does not match 4851-normal.jpg -% C:\foo\bar\baz.jpg matches baz.jpg -function isMatch = strictMatch(a, b) -a = lower(a); -b = lower(b); - -[~, aBase, aExt] = fileparts(a); -[~, bBase, bExt] = fileparts(b); - -isMatch = strcmp(aBase, bBase) && strcmp(aExt, bExt); diff --git a/BatchRenderer/AssimpStrategy/RtbAssimpStrategy.m b/BatchRenderer/AssimpStrategy/RtbAssimpStrategy.m index 1e7e1b9495a533cb4206cdb86c22b7e725861c91..9400b8fa9bc1ac0ebf07f88d164c2debb4eb748a 100644 --- a/BatchRenderer/AssimpStrategy/RtbAssimpStrategy.m +++ b/BatchRenderer/AssimpStrategy/RtbAssimpStrategy.m @@ -137,7 +137,13 @@ classdef RtbAssimpStrategy < RtbBatchRenderStrategy scene = mexximpLoad(sceneFile); else % import anew - scene = mexximpCleanImport(sceneFile, obj.importArgs{:}); + resourceFolder = rtbWorkingFolder( ... + 'folderName', 'resources', ... + 'rendererSpecific', false, ... + 'hints', obj.hints); + scene = mexximpCleanImport(sceneFile, ... + 'workingFolder', resourceFolder, ... + obj.importArgs{:}); end end @@ -169,41 +175,45 @@ classdef RtbAssimpStrategy < RtbBatchRenderStrategy function [scene, mappings] = resolveResources(obj, scene, mappings) % locate files and fix up names - resourceFolder = rtbWorkingFolder( ... - 'folderName', 'resources', ... + workingFolder = rtbWorkingFolder( ... 'rendererSpecific', false, ... 'hints', obj.hints); - mappings = mexximpVisitStructFields(mappings, @rtbResourcePath, ... + mappings = mexximpVisitStructFields(mappings, @mexximpResolveResource, ... 'filterFunction', @RtbAssimpStrategy.mightBeFile, ... 'ignoreFields', {'rootNode', 'embeddedTextures', 'meshes', 'lights', 'cameras'}, ... 'visitArgs', { ... 'strictMatching', true, ... - 'resourceFolder', resourceFolder, ... - 'writeFullPaths', false, ... - 'relativePath', 'resources', ... - 'toReplace', ':-', ... - 'copyOnReplace', true}); - scene = mexximpVisitStructFields(scene, @rtbResourcePath, ... + 'useMatlabPath', true, ... + 'sourceFolder', workingFolder, ... + 'outputFolder', workingFolder, ... + 'outputPrefix', 'resources', ... + 'outputReplaceCharacters', ':-', ... + 'outputReplaceWith', '_'}); + scene = mexximpVisitStructFields(scene, @mexximpResolveResource, ... 'filterFunction', @RtbAssimpStrategy.mightBeFile, ... 'ignoreFields', {'rootNode', 'embeddedTextures', 'meshes', 'lights', 'cameras'}, ... 'visitArgs', { ... 'strictMatching', true, ... - 'resourceFolder', resourceFolder, ... - 'writeFullPaths', false, ... - 'relativePath', 'resources', ... - 'toReplace', ':-', ... - 'copyOnReplace', true}); + 'sourceFolder', workingFolder, ... + 'outputFolder', workingFolder, ... + 'outputPrefix', 'resources', ... + 'outputReplaceCharacters', ':-', ... + 'outputReplaceWith', '_'}); mappings = mexximpVisitStructFields(mappings, @mexximpRecodeImage, ... 'filterFunction', @RtbAssimpStrategy.mightBeFile, ... 'ignoreFields', {'rootNode', 'embeddedTextures', 'meshes', 'lights', 'cameras'}, ... 'visitArgs', { ... + 'sceneFolder', workingFolder, ... + 'skipExisting', true, ... 'toReplace', {'gif'}, ... 'targetFormat', 'png'}); scene = mexximpVisitStructFields(scene, @mexximpRecodeImage, ... 'filterFunction', @RtbAssimpStrategy.mightBeFile, ... 'ignoreFields', {'rootNode', 'embeddedTextures', 'meshes', 'lights', 'cameras'}, ... 'visitArgs', { ... + 'sceneFolder', workingFolder, ... + 'skipExisting', true, ... 'toReplace', {'gif'}, ... 'targetFormat', 'png'}); end diff --git a/BatchRenderer/AssimpStrategy/MitsubaConverter/RtbAssimpMitsubaConverter.m b/BatchRenderer/Renderers/Mitsuba/MitsubaConverter/RtbAssimpMitsubaConverter.m similarity index 100% rename from BatchRenderer/AssimpStrategy/MitsubaConverter/RtbAssimpMitsubaConverter.m rename to BatchRenderer/Renderers/Mitsuba/MitsubaConverter/RtbAssimpMitsubaConverter.m diff --git a/BatchRenderer/AssimpStrategy/MitsubaConverter/rtbApplyMMitsubaGenericMappings.m b/BatchRenderer/Renderers/Mitsuba/MitsubaConverter/rtbApplyMMitsubaGenericMappings.m similarity index 100% rename from BatchRenderer/AssimpStrategy/MitsubaConverter/rtbApplyMMitsubaGenericMappings.m rename to BatchRenderer/Renderers/Mitsuba/MitsubaConverter/rtbApplyMMitsubaGenericMappings.m diff --git a/BatchRenderer/AssimpStrategy/MitsubaConverter/rtbApplyMMitsubaMappingOperation.m b/BatchRenderer/Renderers/Mitsuba/MitsubaConverter/rtbApplyMMitsubaMappingOperation.m similarity index 100% rename from BatchRenderer/AssimpStrategy/MitsubaConverter/rtbApplyMMitsubaMappingOperation.m rename to BatchRenderer/Renderers/Mitsuba/MitsubaConverter/rtbApplyMMitsubaMappingOperation.m diff --git a/BatchRenderer/AssimpStrategy/MitsubaConverter/rtbApplyMMitsubaMappings.m b/BatchRenderer/Renderers/Mitsuba/MitsubaConverter/rtbApplyMMitsubaMappings.m similarity index 100% rename from BatchRenderer/AssimpStrategy/MitsubaConverter/rtbApplyMMitsubaMappings.m rename to BatchRenderer/Renderers/Mitsuba/MitsubaConverter/rtbApplyMMitsubaMappings.m diff --git a/BatchRenderer/AssimpStrategy/MitsubaConverter/rtbMMitsubaBlessAsAreaLight.m b/BatchRenderer/Renderers/Mitsuba/MitsubaConverter/rtbMMitsubaBlessAsAreaLight.m similarity index 100% rename from BatchRenderer/AssimpStrategy/MitsubaConverter/rtbMMitsubaBlessAsAreaLight.m rename to BatchRenderer/Renderers/Mitsuba/MitsubaConverter/rtbMMitsubaBlessAsAreaLight.m diff --git a/BatchRenderer/AssimpStrategy/MitsubaConverter/rtbMMitsubaBlessAsBumpMap.m b/BatchRenderer/Renderers/Mitsuba/MitsubaConverter/rtbMMitsubaBlessAsBumpMap.m similarity index 100% rename from BatchRenderer/AssimpStrategy/MitsubaConverter/rtbMMitsubaBlessAsBumpMap.m rename to BatchRenderer/Renderers/Mitsuba/MitsubaConverter/rtbMMitsubaBlessAsBumpMap.m diff --git a/BatchRenderer/Renderers/RtbMitsubaRenderer.m b/BatchRenderer/Renderers/Mitsuba/RtbMitsubaRenderer.m similarity index 100% rename from BatchRenderer/Renderers/RtbMitsubaRenderer.m rename to BatchRenderer/Renderers/Mitsuba/RtbMitsubaRenderer.m diff --git a/BatchRenderer/Renderers/Mitsuba/rtbRenderMitsubaFactoids.m b/BatchRenderer/Renderers/Mitsuba/rtbRenderMitsubaFactoids.m new file mode 100644 index 0000000000000000000000000000000000000000..e67959e8879ba242fb8be0d6f8a22aa2c9676bf4 --- /dev/null +++ b/BatchRenderer/Renderers/Mitsuba/rtbRenderMitsubaFactoids.m @@ -0,0 +1,93 @@ +function [factoids, exrOutput] = rtbRenderMitsubaFactoids(sceneFile, varargin) +% Obtain ground truth "factoids" about a Mitsuba scene. +% +% [factoids, exrOutput] = rtbRenderMitsubaFactoids(sceneFile) invokes +% Mitsuba to obtain ground truth scene "factoids". Returns a struct array +% of ground truth images, with one field per ground truth factoid. +% +% The given sceneFile must specify a "multichannel" integrator with one or +% more nested "field" integrators. You can create such scenes with +% rtbWriteMitsubaFactoidScene(). +% +% rtbRenderMitsubaFactoids( ... 'mitsuba', mitsuba) specify a struct of +% info about the installed Mitsuba renderer. For some factoids, this must +% be a version of Mistuba compiled for RGB rendering, not spectral +% rendering. The default is taken from getpref('Mitsuba'). +% +% rtbRenderMitsubaFactoids(... 'hints', hints) +% Specify a struct of RenderToolbox options. If hints is omitted, values +% are taken from rtbDefaultHints(). +% +%%% RenderToolbox4 Copyright (c) 2012-2017 The RenderToolbox Team. +%%% About Us://github.com/RenderToolbox/RenderToolbox4/wiki/About-Us +%%% RenderToolbox4 is released under the MIT License. See LICENSE file. + +parser = inputParser(); +parser.addRequired('sceneFile', @ischar); +parser.addParameter('mitsuba', [], @(m) isempty(m) || isstruct(m)); +parser.addParameter('hints', rtbDefaultHints(), @isstruct); +parser.parse(sceneFile, varargin{:}); +sceneFile = parser.Results.sceneFile; +mitsuba = parser.Results.mitsuba; +hints = rtbDefaultHints(parser.Results.hints); + +if isempty(mitsuba) + % modify default mitsuba config to look for rgb + mitsuba = getpref('Mitsuba'); + mitsuba.dockerImage = 'ninjaben/mitsuba-rgb'; + mitsuba.kubernetesPodSelector = 'app=mitsuba-spectral'; + if ismac() + mitsuba.app = '/Applications/Mitsuba-RGB.app'; + else + mitsuba.executable = 'mitusba-rgb'; + end +end + +% look carefully for the input file +workingFolder = rtbWorkingFolder('hints', hints); +fileInfo = rtbResolveFilePath(sceneFile, workingFolder); +sceneFile = fileInfo.absolutePath; + + +%% Render the factoid scene.renderer = RtbMitsubaRenderer(hints); +renderer = RtbMitsubaRenderer(hints); +renderer.mitsuba = mitsuba; + +[~, ~, exrOutput] = renderer.renderToExr(sceneFile); +[sliceInfo, data] = ReadMultichannelEXR(exrOutput); + + +%% Group data slices by factoid name. +factoids = struct(); +factoidSize = size(data); +for ii = 1:numel(sliceInfo) + % factoid channels have names like albedo.R, albedo.G, albedo.B + split = find(sliceInfo(ii).name == '.'); + factoidName = sliceInfo(ii).name(1:split-1); + channelName = sliceInfo(ii).name(split+1:end); + + % initialize factoid output with data array and channel names + if ~isfield(factoids, factoidName) + factoids.(factoidName).data = ... + zeros(factoidSize(1), factoidSize(2), 0); + factoids.(factoidName).channels = {}; + end + + % sort channels, which may arrive out of order + switch channelName + case 'R' + dataIndex = 1; + case 'G' + dataIndex = 2; + case 'B' + dataIndex = 3; + otherwise + dataIndex = numel(factoids.(factoidName).channels) + 1; + end + + % insert data and channel name into output for this factoid + slice = data(:,:,ii); + factoids.(factoidName).data(:, :, dataIndex) = slice; + factoids.(factoidName).channels{dataIndex} = channelName; +end + diff --git a/BatchRenderer/Renderers/Mitsuba/rtbWriteMitsubaFactoidScene.m b/BatchRenderer/Renderers/Mitsuba/rtbWriteMitsubaFactoidScene.m new file mode 100644 index 0000000000000000000000000000000000000000..e6c909086366f31b40456190f11feaadd5b5202e --- /dev/null +++ b/BatchRenderer/Renderers/Mitsuba/rtbWriteMitsubaFactoidScene.m @@ -0,0 +1,135 @@ +function factoidFile = rtbWriteMitsubaFactoidScene(originalFile, varargin) +% Convert the given scene to get factoids instead of ray tracing. +% +% factoidFile = rtbWriteMitsubaFactoidScene(originalFile) copies and +% modifies the given originalFile so that it will produce Mitsuba ground +% truth "factoids" instead of ray tracing data. Returns a new Mitsuba +% scene file based on the given originalFile. +% +% The returned sceneFile woill specify a "multichannel" integrator with one +% or more nested "field" integrators. You can pass this file to +% rtbRenderMitsubaFactoids() to obtain the factoid data. +% +% rtbWriteMitsubaFactoidScene( ... 'factoids', factoids) specify a cell +% array of ground truth factoid names to be obtained. The default includes +% all available factoids: +% - 'position' - absolute position of the object under each pixel +% - 'relPosition' - camera-relative position of the object under each pixel +% - 'distance' - distance to camera of the object under each pixel +% - 'geoNormal' - surface normal at the surface under each pixel +% - 'shNormal' - surface normal at the surface under each pixel, interpolated for shading +% - 'uv' - texture mapping UV coordinates at the surface under each pixel +% - 'albedo' - diffuse reflectance of the object under each pixel +% - 'shapeIndex' - integer identifier for the object under each pixel +% - 'primIndex' - integer identifier for the triangle or other primitive under each pixel +% +% rtbWriteMitsubaFactoidScene( ... 'factoidFormat', factoidFormat) specify +% a mitsuba pixel format to use for formatting the output, like 'rgb' or +% 'spectrum'. The default is 'rgb'. +% +% rtbWriteMitsubaFactoidScene( ... 'singleSampling', singleSampling) +% whether or not to do a simplified rendering with one sample per pixel and +% a narrow "box" reconstruction filder. This is useful for labeling +% factoids like shapeIndex, where it doesn't make sense to average across +% multiple ray samples. The default is true, do a simplified rendering. +% +%%% RenderToolbox4 Copyright (c) 2012-2017 The RenderToolbox Team. +%%% About Us://github.com/RenderToolbox/RenderToolbox4/wiki/About-Us +%%% RenderToolbox4 is released under the MIT License. See LICENSE file. + +parser = inputParser(); +parser.addRequired('originalFile', @ischar); +parser.addParameter('factoidFile', '', @ischar); +parser.addParameter('factoids', ... + {'position', 'relPosition', 'distance', 'geoNormal', 'shNormal', ... + 'uv', 'albedo', 'shapeIndex', 'primIndex'}, ... + @iscellstr); +parser.addParameter('factoidFormat', 'rgb', @ischar); +parser.addParameter('singleSampling', true, @islogical); +parser.addParameter('hints', rtbDefaultHints(), @isstruct); +parser.parse(originalFile, varargin{:}); +originalFile = parser.Results.originalFile; +factoidFile = parser.Results.factoidFile; +factoids = parser.Results.factoids; +factoidFormat = parser.Results.factoidFormat; +singleSampling = parser.Results.singleSampling; +hints = rtbDefaultHints(parser.Results.hints); + +% look carefully for the input file +workingFolder = rtbWorkingFolder('hints', hints); +fileInfo = rtbResolveFilePath(originalFile, workingFolder); +originalFile = fileInfo.absolutePath; + +% default output like the input +if isempty(factoidFile) + [factoidPath, factoidBase] = fileparts(originalFile); + factoidFile = fullfile(factoidPath, [factoidBase '-factoids.xml']); +end + + +%% Read the in the scene xml document. +sceneDocument = xml2struct(originalFile); + + +%% Replace the integrator for multiple "fields". + +% "multichannel" integrator to hold several "fields" +integrator.Attributes.id = 'integrator'; +integrator.Attributes.type = 'multichannel'; +sceneDocument.scene.integrator = integrator; + +% nested "field" for each factoid +nFactoids = numel(factoids); +fieldIntegrators = cell(1, nFactoids); +for ff = 1:nFactoids + factoidName = factoids{ff}; + + fieldIntegrator = struct(); + fieldIntegrator.Attributes.name = factoidName; + fieldIntegrator.Attributes.type = 'field'; + fieldIntegrator.string.Attributes.name = 'field'; + fieldIntegrator.string.Attributes.value = factoidName; + + fieldIntegrators{ff} = fieldIntegrator; +end +sceneDocument.scene.integrator.integrator = fieldIntegrators; + + +%% Replace the film for exr and factoid formats. +sceneDocument.scene.sensor.film.Attributes.type = 'hdrfilm'; +filmStrings = cell(1, 4); +filmStrings{1}.Attributes.name = 'componentFormat'; +filmStrings{1}.Attributes.value = 'float16'; +filmStrings{2}.Attributes.name = 'fileFormat'; +filmStrings{2}.Attributes.value = 'openexr'; + +[formatCell{1:nFactoids}] = deal(factoidFormat); +formatList = sprintf('%s, ', formatCell{:}); +filmStrings{3}.Attributes.name = 'pixelFormat'; +filmStrings{3}.Attributes.value = formatList(1:end-2); + +nameList = sprintf('%s, ', factoids{:}); +filmStrings{4}.Attributes.name = 'channelNames'; +filmStrings{4}.Attributes.value = nameList(1:end-2); + +sceneDocument.scene.sensor.film.string = filmStrings; + + +%% Replace the filter and sampler for simplified rendering? +if singleSampling + sampler.Attributes.id = 'sampler'; + sampler.Attributes.type = 'ldsampler'; + sampler.integer.Attributes.name = 'sampleCount'; + sampler.integer.Attributes.value = 1; + sceneDocument.scene.sensor.sampler = sampler; + + rfilter.Attributes.id = 'rfilter'; + rfilter.Attributes.type = 'box'; + rfilter.float.Attributes.name = 'radius'; + rfilter.float.Attributes.value= 0.5; + sceneDocument.scene.sensor.film.rfilter = rfilter; +end + + +%% Write back the scene document. +struct2xml(sceneDocument, factoidFile); diff --git a/BatchRenderer/AssimpStrategy/PBRTConverter/RtbAssimpPBRTConverter.m b/BatchRenderer/Renderers/PBRT/PBRTConverter/RtbAssimpPBRTConverter.m similarity index 85% rename from BatchRenderer/AssimpStrategy/PBRTConverter/RtbAssimpPBRTConverter.m rename to BatchRenderer/Renderers/PBRT/PBRTConverter/RtbAssimpPBRTConverter.m index baed1bc3e924c6b71f6503690e061b4741defec2..cf6c479e3f903dcab27e3fc727b1cac86dcd63b0 100644 --- a/BatchRenderer/AssimpStrategy/PBRTConverter/RtbAssimpPBRTConverter.m +++ b/BatchRenderer/Renderers/PBRT/PBRTConverter/RtbAssimpPBRTConverter.m @@ -15,6 +15,18 @@ classdef RtbAssimpPBRTConverter < RtbConverter % material parameter to receive specular reflectance data specularParameter; + % material parameter to receive glossy reflectance data + glossyParameter + + % material parameter to receive index of refraction + iorParameter + + % material parameter to receive roughness of the material + roughnessParameter + + % material parameter to receive opacity of the material + opacityParameter + % where to write output files, like scene or geometry outputFolder; @@ -27,8 +39,14 @@ classdef RtbAssimpPBRTConverter < RtbConverter methods (Static) function material = defaultMaterial() - material = MPbrtElement.makeNamedMaterial('', 'matte'); + material = MPbrtElement.makeNamedMaterial('', 'uber'); material.setParameter('Kd', 'spectrum', '300:1 800:1'); + material.setParameter('Ks', 'spectrum', '300:0 800:0'); + material.setParameter('Kr', 'spectrum', '300:0 800:0'); + material.setParameter('roughness','float',0.1); + material.setParameter('index','float',1.5); + material.setParameter('opacity', 'spectrum', '300:1 800:1'); + end end @@ -38,7 +56,11 @@ classdef RtbAssimpPBRTConverter < RtbConverter obj.hints = rtbDefaultHints(hints); obj.material = RtbAssimpPBRTConverter.defaultMaterial(); obj.diffuseParameter = 'Kd'; - obj.specularParameter = ''; + obj.specularParameter = 'Ks'; + obj.glossyParameter = 'Kr'; + obj.iorParameter = 'index'; + obj.roughnessParameter = 'roughness'; + obj.opacityParameter = 'opacity'; obj.outputFolder = rtbWorkingFolder('hints', obj.hints); obj.meshSubfolder = 'scenes/PBRT/pbrt-geometry'; obj.rewriteMeshData = true; @@ -118,6 +140,10 @@ classdef RtbAssimpPBRTConverter < RtbConverter 'materialDefault', obj.material, ... 'materialDiffuseParameter', obj.diffuseParameter, ... 'materialSpecularParameter', obj.specularParameter, ... + 'materialGlossyParameter', obj.glossyParameter, ... + 'materialIorParameter', obj.iorParameter, ... + 'materialOpacityParameter', obj.opacityParameter, ... + 'materialRoughnessParamter', obj.roughnessParameter, ... 'workingFolder', obj.outputFolder, ... 'meshSubfolder', obj.meshSubfolder, ... 'rewriteMeshData', obj.rewriteMeshData); diff --git a/BatchRenderer/AssimpStrategy/PBRTConverter/rtbApplyMPbrtGenericMappings.m b/BatchRenderer/Renderers/PBRT/PBRTConverter/rtbApplyMPbrtGenericMappings.m similarity index 100% rename from BatchRenderer/AssimpStrategy/PBRTConverter/rtbApplyMPbrtGenericMappings.m rename to BatchRenderer/Renderers/PBRT/PBRTConverter/rtbApplyMPbrtGenericMappings.m diff --git a/BatchRenderer/AssimpStrategy/PBRTConverter/rtbApplyMPbrtMappingOperation.m b/BatchRenderer/Renderers/PBRT/PBRTConverter/rtbApplyMPbrtMappingOperation.m similarity index 100% rename from BatchRenderer/AssimpStrategy/PBRTConverter/rtbApplyMPbrtMappingOperation.m rename to BatchRenderer/Renderers/PBRT/PBRTConverter/rtbApplyMPbrtMappingOperation.m diff --git a/BatchRenderer/AssimpStrategy/PBRTConverter/rtbApplyMPbrtMappings.m b/BatchRenderer/Renderers/PBRT/PBRTConverter/rtbApplyMPbrtMappings.m similarity index 100% rename from BatchRenderer/AssimpStrategy/PBRTConverter/rtbApplyMPbrtMappings.m rename to BatchRenderer/Renderers/PBRT/PBRTConverter/rtbApplyMPbrtMappings.m diff --git a/BatchRenderer/AssimpStrategy/PBRTConverter/rtbMPbrtBlessAsAreaLight.m b/BatchRenderer/Renderers/PBRT/PBRTConverter/rtbMPbrtBlessAsAreaLight.m similarity index 100% rename from BatchRenderer/AssimpStrategy/PBRTConverter/rtbMPbrtBlessAsAreaLight.m rename to BatchRenderer/Renderers/PBRT/PBRTConverter/rtbMPbrtBlessAsAreaLight.m diff --git a/BatchRenderer/AssimpStrategy/PBRTConverter/rtbMPbrtBlessAsBumpMap.m b/BatchRenderer/Renderers/PBRT/PBRTConverter/rtbMPbrtBlessAsBumpMap.m similarity index 100% rename from BatchRenderer/AssimpStrategy/PBRTConverter/rtbMPbrtBlessAsBumpMap.m rename to BatchRenderer/Renderers/PBRT/PBRTConverter/rtbMPbrtBlessAsBumpMap.m diff --git a/BatchRenderer/Renderers/RtbPBRTRenderer.m b/BatchRenderer/Renderers/PBRT/RtbPBRTRenderer.m similarity index 100% rename from BatchRenderer/Renderers/RtbPBRTRenderer.m rename to BatchRenderer/Renderers/PBRT/RtbPBRTRenderer.m diff --git a/BatchRenderer/Renderers/RtbSampleRendererRenderer.m b/BatchRenderer/Renderers/SampleRenderer/RtbSampleRendererRenderer.m similarity index 100% rename from BatchRenderer/Renderers/RtbSampleRendererRenderer.m rename to BatchRenderer/Renderers/SampleRenderer/RtbSampleRendererRenderer.m diff --git a/BatchRenderer/AssimpStrategy/SampleRendererConverter/RtbAssimpSampleRendererConverter.m b/BatchRenderer/Renderers/SampleRenderer/SampleRendererConverter/RtbAssimpSampleRendererConverter.m similarity index 100% rename from BatchRenderer/AssimpStrategy/SampleRendererConverter/RtbAssimpSampleRendererConverter.m rename to BatchRenderer/Renderers/SampleRenderer/SampleRendererConverter/RtbAssimpSampleRendererConverter.m diff --git a/ExampleScenes/Flythrough/rtbMakeFlythrough.m b/ExampleScenes/Flythrough/rtbMakeFlythrough.m index 3ffec08a25780cb70f0f8a592d097da5dfaa9ec4..ae410b6402ccfbd6fd44c7bfe3286a6a98194f02 100644 --- a/ExampleScenes/Flythrough/rtbMakeFlythrough.m +++ b/ExampleScenes/Flythrough/rtbMakeFlythrough.m @@ -10,15 +10,26 @@ nightClubFile = fullfile(rtbRoot(), 'ExampleScenes', 'Flythrough', 'NightClub', millenniumFalconFile = fullfile(rtbRoot(), 'ExampleScenes', 'Flythrough', 'MilleniumFalcon', 'millenium-falcon.obj'); +%% Choose batch renderer options. +hints.imageWidth = 320; +hints.imageHeight = 240; +hints.fov = deg2rad(60); +hints.recipeName = 'rtbMakeFlythrough'; + +hints.renderer = 'Mitsuba'; +hints.batchRenderStrategy = RtbAssimpStrategy(hints); +hints.batchRenderStrategy.remodelPerConditionAfterFunction = @rtbFlythroughMexximpRemodeler; +hints.batchRenderStrategy.converter.remodelAfterMappingsFunction = @rtbFlythroughMitsubaRemodeler; + +resourceFolder = rtbWorkingFolder('hints', hints, ... + 'folderName', 'resources'); + + %% Build a combined scene with lights and camera. -nightClub = mexximpCleanImport(nightClubFile); -falcon = mexximpCleanImport(millenniumFalconFile); -falcon = mexximpVisitStructFields(falcon, @rtbResourcePath, ... - 'filterFunction', @RtbAssimpStrategy.mightBeFile, ... - 'visitArgs', { ... - 'resourceFolder', fileparts(millenniumFalconFile), ... - 'toReplace', '', ... - 'writeFullPaths', false}); +nightClub = mexximpCleanImport(nightClubFile, ... + 'workingFolder', resourceFolder); +falcon = mexximpCleanImport(millenniumFalconFile, ... + 'workingFolder', resourceFolder); falconSize = 50; insertTransform = mexximpScale(falconSize * [1 1 1]); @@ -33,18 +44,6 @@ scene = mexximpCentralizeCamera(scene, 'viewExterior', false); %mexximpScenePreview(scene); -%% Choose batch renderer options. -hints.imageWidth = 320; -hints.imageHeight = 240; -hints.fov = deg2rad(60); -hints.recipeName = 'rtbMakeFlythrough'; - -hints.renderer = 'Mitsuba'; -hints.batchRenderStrategy = RtbAssimpStrategy(hints); -hints.batchRenderStrategy.remodelPerConditionAfterFunction = @rtbFlythroughMexximpRemodeler; -hints.batchRenderStrategy.converter.remodelAfterMappingsFunction = @rtbFlythroughMitsubaRemodeler; - - %% Choose some waypoints for the camera and falcon movement. falconPositionWaypoints = [ ... 143 54 -44; ... @@ -105,6 +104,7 @@ rtbWriteConditionsFile(conditionsFile, names, values); %% Make Scene files. +hints.whichConditions = 3; nativeSceneFiles = rtbMakeSceneFiles(scene, ... 'conditionsFile', conditionsFile, ... 'hints', hints); diff --git a/ExampleScenes/Interior/rtbMakeInteriorMitsubaFactoids.m b/ExampleScenes/Interior/rtbMakeInteriorMitsubaFactoids.m new file mode 100644 index 0000000000000000000000000000000000000000..fa3929be3af774a3c3449f35dba3509d96eda038 --- /dev/null +++ b/ExampleScenes/Interior/rtbMakeInteriorMitsubaFactoids.m @@ -0,0 +1,80 @@ +%%% RenderToolbox4 Copyright (c) 2012-2017 The RenderToolbox Team. +%%% About Us://github.com/RenderToolbox/RenderToolbox4/wiki/About-Us +%%% RenderToolbox4 is released under the MIT License. See LICENSE file. +% +%% Render the Interior scene with various lighting. + +%% Choose example files, make sure they're on the Matlab path. +parentSceneFile = 'interio.dae'; +conditionsFile = 'InteriorConditions.txt'; +mappingsFile = 'InteriorMappings.json'; + +%% Choose batch renderer options. +hints.whichConditions = 1; +hints.fov = 49.13434 * pi() / 180; +hints.recipeName = 'rtbMakeInteriorFactoids'; + +%% Write some spectra to use. +resources = rtbWorkingFolder('folderName', 'resources', 'hints', hints); +cieInfo = load('B_cieday'); + +% make orange-yellow for a few lights +temp = 4000; +scale = 3; +spd = scale * GenerateCIEDay(temp, cieInfo.B_cieday); +wls = SToWls(cieInfo.S_cieday); +rtbWriteSpectrumFile(wls, spd, fullfile(resources, 'YellowLight.spd')); + +% make strong yellow for the hanging spot light +temp = 5000; +scale = 30; +spd = scale * GenerateCIEDay(temp, cieInfo.B_cieday); +wls = SToWls(cieInfo.S_cieday); +rtbWriteSpectrumFile(wls, spd, fullfile(resources, 'HangingLight.spd')); + +% make daylight for the windows behind the camera +[wavelengths, magnitudes] = rtbReadSpectrum('D65.spd'); +scale = 1; +magnitudes = scale * magnitudes; +rtbWriteSpectrumFile(wavelengths, magnitudes, fullfile(resources, 'WindowLight.spd')); + +%% Obtain factoids with Mitsuba. + +% start by generating a regular scene file. +hints.renderer = 'Mitsuba'; +nativeSceneFiles = rtbMakeSceneFiles(parentSceneFile, ... + 'mappingsFile', mappingsFile, ... + 'conditionsFile', conditionsFile, ... + 'hints', hints); + +% convert regular scene file for factoid rendering +factoidSceneFile = rtbWriteMitsubaFactoidScene(nativeSceneFiles{1}, ... + 'hints', hints); + +% invoke Mitsuba to get the factoids +factoids = rtbRenderMitsubaFactoids(factoidSceneFile, ... + 'hints', hints); + +%% Plot the factoids. +factoidNames = fieldnames(factoids); +nFactoids = numel(factoidNames); +rows = 3; +columns = ceil(nFactoids / rows); +for ff = 1:nFactoids + factoidName = factoidNames{ff}; + factoid = factoids.(factoidName); + + subplot(rows, columns, ff); + + switch factoidName + case {'primIndex', 'shapeIndex'} + % index/nominal data with colormap + imshow(factoid.data(:,:,1), prism()); + otherwise + % continuous data as stretched rgb + stretchedRgb = factoid.data ./ max(factoid.data(:)); + imshow(stretchedRgb); + end + + title(factoidName); +end diff --git a/ExampleScenes/WildScene/rtbMakeWildScene.m b/ExampleScenes/WildScene/rtbMakeWildScene.m index 3fdfbe9d435c350b515653efeda603c994455140..b76979da48518ebf1c96717df60a23f4302aac28 100644 --- a/ExampleScenes/WildScene/rtbMakeWildScene.m +++ b/ExampleScenes/WildScene/rtbMakeWildScene.m @@ -26,12 +26,24 @@ clear; clc; + pathHere = fileparts(which('rtbMakeWildScene')); +%% Choose batch processing options. +hints.imageWidth = 640; +hints.imageHeight = 480; +hints.recipeName = 'rtbMakeWildScene'; +hints.renderer = 'Mitsuba'; +hints.batchRenderStrategy = RtbAssimpStrategy(hints); + +resourceFolder = rtbWorkingFolder('hints', hints, ... + 'folderName', 'resources'); + %% Choose a scene format -- how does the 3DS Max scene look? wildScene = fullfile(pathHere, 'millenium-falcon.3DS'); -scene = mexximpCleanImport(wildScene); +scene = mexximpCleanImport(wildScene, ... + 'workingFolder', resourceFolder); % look at the vertices in a scatter plot mexximpSceneScatter(scene); @@ -39,7 +51,8 @@ mexximpSceneScatter(scene); %% Choose a better scene format -- the Wavefront Object. wildScene = fullfile(pathHere, 'millenium-falcon.obj'); -scene = mexximpCleanImport(wildScene); +scene = mexximpCleanImport(wildScene, ... + 'workingFolder', resourceFolder); % look at the vertices in a scatter plot mexximpSceneScatter(scene); @@ -68,38 +81,10 @@ scene = mexximpAddLanterns(scene); disp(displayNicelyFormattedStruct(scene, 'scene', '', 50)); -%% Fix broken texture paths. - -% obj version of the scene comes with an mtl material file -% this file contains bad file paths for a Windows user named "glenn" -% for example: -disp(['Original path: ' scene.materials(2).properties(10).data]); - -% we can find and fix paths like this -% by recursively fisiting fields of the scene struct -% and doing some fuzzy matching on names -scene = mexximpVisitStructFields(scene, @rtbResourcePath, ... - 'filterFunction', @RtbAssimpStrategy.mightBeFile, ... - 'visitArgs', {'resourceFolder', pathHere, 'toReplace', ''}); - -% now we should have the file locally -disp(['Local path: ' scene.materials(2).properties(10).data]); - - -%% Choose batch processing options. -hints.imageWidth = 640; -hints.imageHeight = 480; -hints.fov = scene.cameras(1).horizontalFov; -hints.recipeName = 'rtbMakeWildScene'; -hints.renderer = 'Mitsuba'; -hints.batchRenderStrategy = RtbAssimpStrategy(hints); - - %% Render with Mitsuba. -% fix broken texture paths - % make a scene file and render it +hints.fov = scene.cameras(1).horizontalFov; nativeSceneFiles = rtbMakeSceneFiles(scene, 'hints', hints); radianceDataFiles = rtbBatchRender(nativeSceneFiles, 'hints', hints); @@ -117,11 +102,9 @@ rtbShowXYZAndSRGB([], sRgb, montageName); %% Choose a nicer viewing axis and render again. -scene = mexximpCleanImport(wildScene, 'flipUVs', true); -scene = mexximpVisitStructFields(scene, @rtbResourcePath, ... - 'filterFunction', @RtbAssimpStrategy.mightBeFile, ... - 'visitArgs', {'resourceFolder', pathHere, 'toReplace', ''}); - +scene = mexximpCleanImport(wildScene, ... + 'workingFolder', resourceFolder, ... + 'flipUVs', true); viewAxis = [-1 1 1]; scene = mexximpCentralizeCamera(scene, 'viewAxis', viewAxis ./ norm(viewAxis)); scene = mexximpAddLanterns(scene); diff --git a/README.md b/README.md index 0ef28221dc0f1240bfc6e4164bc0a400db64427b..dadbf967de5e3f5594f62a5b1a1b3684d9d96b2d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +[](http://50.112.42.141/job/RenderToolbox4/) + RenderToolbox4 ============== RenderToolbox4 is a Matlab toolbox for working with 3D scenes and physically-based renderers. diff --git a/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeCoordinatesTest/renderings/Mitsuba/scene-001.mat b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeCoordinatesTest/renderings/Mitsuba/scene-001.mat new file mode 100644 index 0000000000000000000000000000000000000000..91b97c1fb5bd7b3f3f3430d01789d7853c2c57e1 Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeCoordinatesTest/renderings/Mitsuba/scene-001.mat differ diff --git a/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeCoordinatesTest/renderings/PBRT/scene-001.mat b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeCoordinatesTest/renderings/PBRT/scene-001.mat new file mode 100644 index 0000000000000000000000000000000000000000..87847b68e8bb9a9c672bf3f862a1f794973df60a Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeCoordinatesTest/renderings/PBRT/scene-001.mat differ diff --git a/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereBumps/renderings/Mitsuba/materialSphereMatte.mat b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereBumps/renderings/Mitsuba/materialSphereMatte.mat new file mode 100644 index 0000000000000000000000000000000000000000..147757056e2f6d9761765ad2b533fe88fa5fa65a Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereBumps/renderings/Mitsuba/materialSphereMatte.mat differ diff --git a/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereBumps/renderings/Mitsuba/materialSphereMetal.mat b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereBumps/renderings/Mitsuba/materialSphereMetal.mat new file mode 100644 index 0000000000000000000000000000000000000000..ec74afcd8290b20e9f36097cba384fe64cb5c3d8 Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereBumps/renderings/Mitsuba/materialSphereMetal.mat differ diff --git a/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereBumps/renderings/Mitsuba/materialSphereWard.mat b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereBumps/renderings/Mitsuba/materialSphereWard.mat new file mode 100644 index 0000000000000000000000000000000000000000..ab258e33129912f2dad8db80ce287b2d46accd4b Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereBumps/renderings/Mitsuba/materialSphereWard.mat differ diff --git a/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereBumps/renderings/PBRT/materialSphereMatte.mat b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereBumps/renderings/PBRT/materialSphereMatte.mat new file mode 100644 index 0000000000000000000000000000000000000000..ae0d5c3171187ac6cf9f51b15d0831f13912d524 Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereBumps/renderings/PBRT/materialSphereMatte.mat differ diff --git a/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereBumps/renderings/PBRT/materialSphereMetal.mat b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereBumps/renderings/PBRT/materialSphereMetal.mat new file mode 100644 index 0000000000000000000000000000000000000000..581b93e5b347f15f1ba7d0cdecaaea7ec0cc5a3d Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereBumps/renderings/PBRT/materialSphereMetal.mat differ diff --git a/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereBumps/renderings/PBRT/materialSphereWard.mat b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereBumps/renderings/PBRT/materialSphereWard.mat new file mode 100644 index 0000000000000000000000000000000000000000..531b93f5db0a6607ae6846f8c1ee2beee5dc0aff Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereBumps/renderings/PBRT/materialSphereWard.mat differ diff --git a/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereRemodeled/renderings/Mitsuba/materialSphereMatte.mat b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereRemodeled/renderings/Mitsuba/materialSphereMatte.mat new file mode 100644 index 0000000000000000000000000000000000000000..9581989dddc44c36f608e625b8f8d5526b23cebe Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereRemodeled/renderings/Mitsuba/materialSphereMatte.mat differ diff --git a/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereRemodeled/renderings/Mitsuba/materialSphereMetal.mat b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereRemodeled/renderings/Mitsuba/materialSphereMetal.mat new file mode 100644 index 0000000000000000000000000000000000000000..68288e28e102fc195ba0b794eae61485c586d794 Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereRemodeled/renderings/Mitsuba/materialSphereMetal.mat differ diff --git a/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereRemodeled/renderings/Mitsuba/materialSphereWard.mat b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereRemodeled/renderings/Mitsuba/materialSphereWard.mat new file mode 100644 index 0000000000000000000000000000000000000000..de843d723570e18cb2e736e1044026f0fd93a5a0 Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereRemodeled/renderings/Mitsuba/materialSphereWard.mat differ diff --git a/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereRemodeled/renderings/PBRT/materialSphereMatte.mat b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereRemodeled/renderings/PBRT/materialSphereMatte.mat new file mode 100644 index 0000000000000000000000000000000000000000..db91b26ce2d86e0a604c259679c02dc950380ee6 Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereRemodeled/renderings/PBRT/materialSphereMatte.mat differ diff --git a/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereRemodeled/renderings/PBRT/materialSphereMetal.mat b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereRemodeled/renderings/PBRT/materialSphereMetal.mat new file mode 100644 index 0000000000000000000000000000000000000000..141ea0c82a44a3f8213304a0a8d7aa24bac68324 Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereRemodeled/renderings/PBRT/materialSphereMetal.mat differ diff --git a/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereRemodeled/renderings/PBRT/materialSphereWard.mat b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereRemodeled/renderings/PBRT/materialSphereWard.mat new file mode 100644 index 0000000000000000000000000000000000000000..23ffa17b5bac30b8dcf5c4c2d18e1d66ebce4afb Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereRemodeled/renderings/PBRT/materialSphereWard.mat differ diff --git a/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeCoordinatesTest/renderings/Mitsuba/scene-001.mat b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeCoordinatesTest/renderings/Mitsuba/scene-001.mat new file mode 100644 index 0000000000000000000000000000000000000000..ed07771aae9652a555818dfe65c126dd304bffb9 Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeCoordinatesTest/renderings/Mitsuba/scene-001.mat differ diff --git a/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeCoordinatesTest/renderings/PBRT/scene-001.mat b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeCoordinatesTest/renderings/PBRT/scene-001.mat new file mode 100644 index 0000000000000000000000000000000000000000..b1d7f2875e428ab210b346360b25c78e8f62c9de Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeCoordinatesTest/renderings/PBRT/scene-001.mat differ diff --git a/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereBumps/renderings/Mitsuba/materialSphereMatte.mat b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereBumps/renderings/Mitsuba/materialSphereMatte.mat new file mode 100644 index 0000000000000000000000000000000000000000..a5c6d16e3fb02ff1678e85120260cac738e1c849 Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereBumps/renderings/Mitsuba/materialSphereMatte.mat differ diff --git a/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereBumps/renderings/Mitsuba/materialSphereMetal.mat b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereBumps/renderings/Mitsuba/materialSphereMetal.mat new file mode 100644 index 0000000000000000000000000000000000000000..2c66232fd55136020ebebc16e966f274763d252d Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereBumps/renderings/Mitsuba/materialSphereMetal.mat differ diff --git a/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereBumps/renderings/Mitsuba/materialSphereWard.mat b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereBumps/renderings/Mitsuba/materialSphereWard.mat new file mode 100644 index 0000000000000000000000000000000000000000..105f862a6f126f030fd43affa8dcc1bc11c33257 Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereBumps/renderings/Mitsuba/materialSphereWard.mat differ diff --git a/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereBumps/renderings/PBRT/materialSphereMatte.mat b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereBumps/renderings/PBRT/materialSphereMatte.mat new file mode 100644 index 0000000000000000000000000000000000000000..cc408994ce9c36cf910823a70ad931fbfe4f76d2 Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereBumps/renderings/PBRT/materialSphereMatte.mat differ diff --git a/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereBumps/renderings/PBRT/materialSphereMetal.mat b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereBumps/renderings/PBRT/materialSphereMetal.mat new file mode 100644 index 0000000000000000000000000000000000000000..38975361d6d9e56af6c9b38359ed3b0e90823f87 Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereBumps/renderings/PBRT/materialSphereMetal.mat differ diff --git a/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereBumps/renderings/PBRT/materialSphereWard.mat b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereBumps/renderings/PBRT/materialSphereWard.mat new file mode 100644 index 0000000000000000000000000000000000000000..9ab6a5cca1fca86ea126f3385918601ddc0aec25 Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereBumps/renderings/PBRT/materialSphereWard.mat differ diff --git a/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereRemodeled/renderings/Mitsuba/materialSphereMatte.mat b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereRemodeled/renderings/Mitsuba/materialSphereMatte.mat new file mode 100644 index 0000000000000000000000000000000000000000..acbf171682d78a79f713be6925cd5f03054c3c08 Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereRemodeled/renderings/Mitsuba/materialSphereMatte.mat differ diff --git a/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereRemodeled/renderings/Mitsuba/materialSphereMetal.mat b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereRemodeled/renderings/Mitsuba/materialSphereMetal.mat new file mode 100644 index 0000000000000000000000000000000000000000..840feacae1e34bf9beca74a98d148bad6b04cf91 Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereRemodeled/renderings/Mitsuba/materialSphereMetal.mat differ diff --git a/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereRemodeled/renderings/Mitsuba/materialSphereWard.mat b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereRemodeled/renderings/Mitsuba/materialSphereWard.mat new file mode 100644 index 0000000000000000000000000000000000000000..fe4e6ddd9e0b008a921695f01018e4eb70f90e27 Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereRemodeled/renderings/Mitsuba/materialSphereWard.mat differ diff --git a/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereRemodeled/renderings/PBRT/materialSphereMatte.mat b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereRemodeled/renderings/PBRT/materialSphereMatte.mat new file mode 100644 index 0000000000000000000000000000000000000000..066a22de2dc7bbf77a9559701a65ef4b03d8b956 Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereRemodeled/renderings/PBRT/materialSphereMatte.mat differ diff --git a/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereRemodeled/renderings/PBRT/materialSphereMetal.mat b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereRemodeled/renderings/PBRT/materialSphereMetal.mat new file mode 100644 index 0000000000000000000000000000000000000000..caca4383f23a79a1f9129a5b2dfd23034a1c8216 Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereRemodeled/renderings/PBRT/materialSphereMetal.mat differ diff --git a/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereRemodeled/renderings/PBRT/materialSphereWard.mat b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereRemodeled/renderings/PBRT/materialSphereWard.mat new file mode 100644 index 0000000000000000000000000000000000000000..ca18a50d8c7797877026ba535cd31e457364a78e Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereRemodeled/renderings/PBRT/materialSphereWard.mat differ diff --git a/Test/Automated/RtbComparisonTests.m b/Test/Automated/RtbComparisonTests.m new file mode 100644 index 0000000000000000000000000000000000000000..c15c8d7f5a4e99b0f97d0288eb30307d311ede12 --- /dev/null +++ b/Test/Automated/RtbComparisonTests.m @@ -0,0 +1,174 @@ +classdef RtbComparisonTests < matlab.unittest.TestCase + % Test functions that find and compare renderings. + + properties + folderA = fullfile(rtbRoot(), 'Test', 'Automated', 'Fixture', 'Comparison', 'RecipesA'); + folderB = fullfile(rtbRoot(), 'Test', 'Automated', 'Fixture', 'Comparison', 'RecipesB'); + scratchFolder = fullfile(tempdir(), 'RtbComparisonTests'); + end + + methods (TestMethodSetup) + function cleanUpOutput(testCase) + if 7 == exist(testCase.scratchFolder, 'dir') + rmdir(testCase.scratchFolder, 's'); + end + end + end + + + methods (Test) + + function testFindRenderingsSuccess(testCase) + expectedNames = {'rtbMakeCoordinatesTest', ... + 'rtbMakeMaterialSphereBumps', ... + 'rtbMakeMaterialSphereRemodeled'}; + + renderingsA = rtbFindRenderings(testCase.folderA); + recipeNamesA = unique({renderingsA.recipeName}); + testCase.assertEqual(recipeNamesA, expectedNames); + testCase.assertNumElements(renderingsA, 14); + + renderingsB = rtbFindRenderings(testCase.folderB); + recipeNamesB = unique({renderingsB.recipeName}); + testCase.assertEqual(recipeNamesB, expectedNames); + testCase.assertNumElements(renderingsA, 14); + end + + function testFindRenderingsSubset(testCase) + renderings = rtbFindRenderings(testCase.folderA, ... + 'filter', 'CoordinatesTest[^\.]+\.mat$'); + testCase.assertNumElements(renderings, 2); + recipeNames = {renderings.recipeName}; + testCase.assertEqual(strcmp(recipeNames, 'rtbMakeCoordinatesTest'), true(1,2)); + end + + function testFindRenderingsNone(testCase) + renderings = rtbFindRenderings(testCase.folderA, ... + 'filter', 'notagoodpattern'); + testCase.assertEmpty(renderings); + end + + function fetchReferenceSuccess(testCase) + renderings = rtbFetchReferenceData('rtbMakeInterreflection', ... + 'referenceRoot', testCase.scratchFolder); + testCase.assertNumElements(renderings, 6); + recipeNames = {renderings.recipeName}; + testCase.assertEqual(strcmp(recipeNames, 'rtbMakeInterreflection'), true(1,6)); + end + + function fetchReferenceNone(testCase) + renderings = rtbFetchReferenceData('nosuchrecipe', ... + 'referenceRoot', testCase.scratchFolder); + testCase.assertNumElements(renderings, 0); + end + + function compareRenderingToSelf(testCase) + renderings = rtbFindRenderings(testCase.folderA); + comparison = rtbCompareRenderings(renderings(1), renderings(1)); + testCase.assertEqual(comparison.corrcoef, 1, 'AbsTol', 1e-6); + testCase.assertEqual(comparison.relNormDiff.max, 0,'AbsTol', 1e-6); + end + + function compareRenderingToOther(testCase) + renderings = rtbFindRenderings(testCase.folderA); + comparison = rtbCompareRenderings(renderings(1), renderings(2)); + testCase.assertLessThan(comparison.corrcoef, 1); + testCase.assertGreaterThan(comparison.relNormDiff.max, 0); + end + + function compareRenderingPlot(testCase) + renderings = rtbFindRenderings(testCase.folderA); + comparison = rtbCompareRenderings(renderings(1), renderings(1)); + fig = rtbPlotRenderingComparison(comparison); + close(fig); + end + + function compareFolderToSelf(testCase) + comparisons = rtbCompareManyRecipes(testCase.folderA, testCase.folderA, ... + 'fetchReferenceData', false); + testCase.assertNumElements(comparisons, 14); + testCase.assertEqual([comparisons.corrcoef], ones(1, 14), 'AbsTol', 1e-6); + relNormDiff = [comparisons.relNormDiff]; + testCase.assertEqual([relNormDiff.max], zeros(1, 14) ,'AbsTol', 1e-6); + end + + function compareFolderToOther(testCase) + comparisons = rtbCompareManyRecipes(testCase.folderA, testCase.folderB, ... + 'fetchReferenceData', false); + testCase.assertNumElements(comparisons, 14); + % real correlations can be unity, skip correlation test + relNormDiff = [comparisons.relNormDiff]; + testCase.assertGreaterThan([relNormDiff.max], 0); + end + + function compareFolderToReference(testCase) + comparisons = rtbCompareManyRecipes(testCase.folderA, testCase.scratchFolder, ... + 'fetchReferenceData', true); + testCase.assertNumElements(comparisons, 14); + % real correlations can be unity, skip correlation test + relNormDiff = [comparisons.relNormDiff]; + testCase.assertGreaterThan([relNormDiff.max], 0); + end + + function compareFolderPlot(testCase) + comparisons = rtbCompareManyRecipes(testCase.folderA, testCase.folderA, ... + 'fetchReferenceData', false); + fig = rtbPlotManyRecipeComparisons(comparisons); + close(fig); + end + + function epicComparisonPlots(testCase) + [comparisons, ~, figs] = rtbRunEpicComparison(testCase.folderA, testCase.folderA, ... + 'plotSummary', true, ... + 'plotImages', true); + testCase.assertNumElements(comparisons, 14); + testCase.assertNumElements(figs, 14 + 1); + close(figs); + end + + function epicComparisonNoPlots(testCase) + [comparisons, ~, figs] = rtbRunEpicComparison(testCase.folderA, testCase.folderA, ... + 'plotSummary', false, ... + 'plotImages', false); + testCase.assertNumElements(comparisons, 14); + testCase.assertEmpty(figs); + end + + function epicComparisonClosePlots(testCase) + [comparisons, ~, figs] = rtbRunEpicComparison(testCase.folderA, testCase.folderA, ... + 'plotSummary', true, ... + 'plotImages', true, ... + 'closeSummary', true, ... + 'closeImages', true); + testCase.assertNumElements(comparisons, 14); + testCase.assertEmpty(figs); + end + + function epicComparisonSavePlots(testCase) + [comparisons, ~, figs] = rtbRunEpicComparison(testCase.folderA, testCase.folderA, ... + 'plotSummary', true, ... + 'plotImages', true, ... + 'closeSummary', true, ... + 'closeImages', true, ... + 'figureFolder', testCase.scratchFolder, ... + 'summaryName', 'test-summary'); + testCase.assertNumElements(comparisons, 14); + testCase.assertEmpty(figs); + + % summary should have been saved in scratch folder + summaryFig = fullfile(testCase.scratchFolder, 'test-summary.fig'); + summaryPng = fullfile(testCase.scratchFolder, 'test-summary.png'); + testCase.assertEqual(exist(summaryFig, 'file'), 2); + testCase.assertEqual(exist(summaryPng, 'file'), 2); + + % images should have been saved in the scratchFolder + for cc = 1:numel(comparisons) + identifier = comparisons(cc).renderingA.identifier; + imageFig = fullfile(testCase.scratchFolder, [identifier '.fig']); + imagePng = fullfile(testCase.scratchFolder, [identifier '.png']); + testCase.assertEqual(exist(imageFig, 'file'), 2); + testCase.assertEqual(exist(imagePng, 'file'), 2); + end + end + end +end diff --git a/Test/Automated/RtbExampleTests.m b/Test/Automated/RtbExampleTests.m new file mode 100644 index 0000000000000000000000000000000000000000..bd07c83d137b88db3831335fef131c00c8e543ce --- /dev/null +++ b/Test/Automated/RtbExampleTests.m @@ -0,0 +1,110 @@ +classdef RtbExampleTests < matlab.unittest.TestCase + % Execute some of the render toolbox examples. + + properties + outputRoot = fullfile(rtbWorkingFolder(), 'RtbExampleTests'); + referenceRoot = fullfile(rtbWorkingFolder(), 'RtbReference'); + end + + methods (TestMethodSetup) + function cleanUpOutput(testCase) + if 7 == exist(testCase.outputRoot, 'dir') + rmdir(testCase.outputRoot, 's'); + end + + if 7 == exist(testCase.referenceRoot, 'dir') + rmdir(testCase.referenceRoot, 's'); + end + end + end + + methods (Test) + + function testNotAnExample(testCase) + results = rtbRunEpicTest( ... + 'outputRoot', testCase.outputRoot, ... + 'makeFunctions', {'notAnExample.m'}); + testCase.assertFalse(results.isSuccess); + testCase.assertNotEmpty(results.error); + end + + function testCoordinatesTest(testCase) + results = rtbRunEpicTest( ... + 'outputRoot', testCase.outputRoot, ... + 'makeFunctions', {'rtbMakeCoordinatesTest.m'}); + testCase.assertTrue(results.isSuccess); + testCase.assertEmpty(results.error); + + % compare to reference rendering + comparisons = rtbCompareManyRecipes( ... + testCase.outputRoot, ... + testCase.referenceRoot, ... + 'fetchReferenceData', true); + testCase.assertTrue(all([comparisons.isGoodComparison])); + + % Skip check for correlation and pixel differences. + % Because this scene doesn't specify its own materials, it is + % sensitive to the default material import behaviors of Assimp, + % mexximp, mMitsuba, and mPbrt. So correlations and pixel + % differences may be large, even though the coordinate systems + % check out fine (which is the point of this example). + end + + function testDragon(testCase) + results = rtbRunEpicTest( ... + 'outputRoot', testCase.outputRoot, ... + 'makeFunctions', {'rtbMakeDragon.m'}); + testCase.assertTrue(results.isSuccess); + testCase.assertEmpty(results.error); + + % compare to reference rendering + comparisons = rtbCompareManyRecipes( ... + testCase.outputRoot, ... + testCase.referenceRoot, ... + 'fetchReferenceData', true); + testCase.assertTrue(all([comparisons.isGoodComparison])); + testCase.assertTrue(all([comparisons.corrcoef] > 0.75)); + relNormDiffs = [comparisons.relNormDiff]; + testCase.assertTrue(all([relNormDiffs.max] < 2.5)); + testCase.assertTrue(all([relNormDiffs.mean] < 2.5)); + end + + function testMaterialSphereBumps(testCase) + results = rtbRunEpicTest( ... + 'outputRoot', testCase.outputRoot, ... + 'makeFunctions', {'rtbMakeMaterialSphereBumps.m'}); + testCase.assertTrue(results.isSuccess); + testCase.assertEmpty(results.error); + + % compare to reference rendering + comparisons = rtbCompareManyRecipes( ... + testCase.outputRoot, ... + testCase.referenceRoot, ... + 'fetchReferenceData', true); + testCase.assertTrue(all([comparisons.isGoodComparison])); + testCase.assertTrue(all([comparisons.corrcoef] > 0.75)); + relNormDiffs = [comparisons.relNormDiff]; + testCase.assertTrue(all([relNormDiffs.max] < 2.5)); + testCase.assertTrue(all([relNormDiffs.mean] < 2.5)); + end + + function testMaterialSphereRemodeled(testCase) + results = rtbRunEpicTest( ... + 'outputRoot', testCase.outputRoot, ... + 'makeFunctions', {'rtbMakeMaterialSphereRemodeled.m'}); + testCase.assertTrue(results.isSuccess); + testCase.assertEmpty(results.error); + + % compare to reference rendering + comparisons = rtbCompareManyRecipes( ... + testCase.outputRoot, ... + testCase.referenceRoot, ... + 'fetchReferenceData', true); + testCase.assertTrue(all([comparisons.isGoodComparison])); + testCase.assertTrue(all([comparisons.corrcoef] > 0.75)); + relNormDiffs = [comparisons.relNormDiff]; + testCase.assertTrue(all([relNormDiffs.max] < 2.5)); + testCase.assertTrue(all([relNormDiffs.mean] < 2.5)); + end + end +end diff --git a/Test/Interactive/Comparison/rtbCompareManyRecipes.m b/Test/Interactive/Comparison/rtbCompareManyRecipes.m new file mode 100644 index 0000000000000000000000000000000000000000..c620716a43bda6feec90449f4488064055c56aa9 --- /dev/null +++ b/Test/Interactive/Comparison/rtbCompareManyRecipes.m @@ -0,0 +1,121 @@ +function [comparisons, matchInfo] = rtbCompareManyRecipes(folderA, folderB, varargin) +%% Compare paris of renderings across two folders. +% +% comparisons = rtbCompareManyRecipes(folderA, folderB) finds rendering +% data files in the given folderA and folderB and attempts to match up +% pairs of renderings that came from the same recipe and renderer. +% For each pair, computes difference images and statistics. +% +% Returns a struct array of image comparisons, as returned from +% rtbCompareRenderings(). +% +% rtbCompareManyRecipes( ... 'fetchReferenceData', fetchReferenceData) +% specify whether to use Remote Data Toolbox to fetch reference data for +% comparison. The default is true, fetch reference data when there is a +% recipe in folderA that was not found in folderB, and cache the fetched +% data in folderB. +% +%%% RenderToolbox4 Copyright (c) 2012-2017 The RenderToolbox Team. +%%% About Us://github.com/RenderToolbox/RenderToolbox4/wiki/About-Us +%%% RenderToolbox4 is released under the MIT License. See LICENSE file. + +parser = inputParser(); +parser.KeepUnmatched = true; +parser.addRequired('folderA', @ischar); +parser.addRequired('folderB', @ischar); +parser.addParameter('fetchReferenceData', true, @islogical); +parser.parse(folderA, folderB, varargin{:}); +folderA = parser.Results.folderA; +folderB = parser.Results.folderB; +fetchReferenceData = parser.Results.fetchReferenceData; + + +%% Identify renderings and recipes to compare. +renderingsA = rtbFindRenderings(folderA, varargin{:}); +recipeNames = unique({renderingsA.recipeName}); +nRecipes = numel(recipeNames); + +renderingsB = rtbFindRenderings(folderB, varargin{:}); + +%% Compare one recipe at a time, fetch data as necessary. +comparisonsCell = cell(1, nRecipes); +matchInfoCell = cell(1, nRecipes); +for rr = 1:nRecipes + recipeName = recipeNames{rr}; + fprintf('Comparing renderings for recipe <%s>.\n', recipeName); + + isRecipeA = strcmp({renderingsA.recipeName}, recipeName); + recipeRenderingsA = renderingsA(isRecipeA); + + % fetch missing recipe for B? + if isempty(renderingsB) + isRecipeB = false; + else + isRecipeB = strcmp({renderingsB.recipeName}, recipeName); + end + + if any(isRecipeB) + recipeRenderingsB = renderingsB(isRecipeB); + elseif fetchReferenceData + fprintf(' Fetching reference data to <%s>...\n', folderB); + recipeRenderingsB = rtbFetchReferenceData(recipeName, ... + 'referenceRoot', folderB, ... + varargin{:}); + if isempty(recipeRenderingsB) + fprintf(' ...could not fetch, skipping this recipe.\n'); + continue; + else + fprintf(' ...OK.\n'); + end + else + fprintf(' Skipping recipe not found in <%s>.\n', folderB); + continue; + end + + % match pairs of renderings for recipes A and B + info = matchRenderingPairs(recipeRenderingsA, recipeRenderingsB); + fprintf(' Found %d matched pairs of renderings.\n', info.nPairs); + + % run a comparison for each matched pair + pairsCell = cell(1, info.nPairs); + for pp = 1:info.nPairs + fprintf(' %s.\n', info.matchedA(pp).identifier); + pairsCell{pp} = rtbCompareRenderings(info.matchedA(pp), info.matchedB(pp), varargin{:}); + end + comparisonsCell{rr} = [pairsCell{:}]; + matchInfoCell{rr} = info; + + % report on unmatched renderings + if ~isempty(info.unmatchedA) + nUnmatched = info.unmatchedA; + fprintf(' %d renderings in A were not matched in B:\n', nUnmatched); + for uu = 1:nUnmatched + fprintf(' %s\n', info.info.unmatchedA(uu).identifier); + end + end + + if ~isempty(info.unmatchedB) + nUnmatched = info.unmatchedB; + fprintf(' %d renderings in B were not matched in A:\n', nUnmatched); + for uu = 1:nUnmatched + fprintf(' %s\n', info.info.unmatchedB(uu).identifier); + end + end +end +comparisons = [comparisonsCell{:}]; +matchInfo = [matchInfoCell{:}]; + + +%% For pairs of comparable renderings from two sets. +function info = matchRenderingPairs(renderingsA, renderingsB) +identifiersA = {renderingsA.identifier}; +identifiersB = {renderingsB.identifier}; +[~, indexA, indexB] = intersect(identifiersA, identifiersB, 'stable'); +[~, unmatchedIndexA] = setdiff(identifiersA, identifiersB); +[~, unmatchedIndexB] = setdiff(identifiersB, identifiersA); + +info.nPairs = numel(indexA); +info.matchedA = renderingsA(indexA); +info.matchedB = renderingsB(indexB); +info.unmatchedA = renderingsA(unmatchedIndexA); +info.unmatchedB = renderingsB(unmatchedIndexB); diff --git a/Test/Interactive/Comparison/rtbCompareRenderings.m b/Test/Interactive/Comparison/rtbCompareRenderings.m new file mode 100644 index 0000000000000000000000000000000000000000..7ca36617f4c0c11634437b236dda6c28b3a8900c --- /dev/null +++ b/Test/Interactive/Comparison/rtbCompareRenderings.m @@ -0,0 +1,194 @@ +function comparison = rtbCompareRenderings(renderingA, renderingB, varargin) +%% Compare two renderings for difference images and statistics. +% +% comparison = rtbCompareRenderings(renderingA, renderingB) compares the +% given renderingA against the given renderingB. Each must be a rendering +% record as returned from rtbRenderingRecord() or rtbFindRenderings(). +% Returns a comparison struct with many various fields including: +% - A -- the multispectral image from renderingA +% - B -- the multispectral image from renderingB +% - aMinusB -- the multispectral difference image A - B +% - bMinusA -- the multispectral difference image B - A +% - relNormDiff -- the min, mean, and max of per-pixel difference +% - corrcoef -- the overall correlation between A and B +% - error -- any error encountered during comparisons +% +% The relNormDiff is obtained by normalizing A and B each by its max value, +% taking the absolute difference, and dividing the difference by the +% normalized values of A. Finally, values of relNormDiff that are less +% than the given denominatorThreshold are set to nan. This is to avoid +% unfair comparisons when dividing by small values. +% +% rtbCompareRenderings( ... 'denominatorThreshold', denominatorThreshold) +% specify the denominatorThreshold to use when computing the relNormDiff +% image described above. The default is 0.2. +% +%%% RenderToolbox4 Copyright (c) 2012-2017 The RenderToolbox Team. +%%% About Us://github.com/RenderToolbox/RenderToolbox4/wiki/About-Us +%%% RenderToolbox4 is released under the MIT License. See LICENSE file. + +parser = inputParser(); +parser.KeepUnmatched = true; +parser.addRequired('renderingA', @isstruct); +parser.addRequired('renderingB', @isstruct); +parser.addParameter('denominatorThreshold', 0.2, @isnumeric); +parser.parse(renderingA, renderingB, varargin{:}); +renderingA = parser.Results.renderingA; +renderingB = parser.Results.renderingB; +denominatorThreshold = parser.Results.denominatorThreshold; + + +%% Consistent struct fields for output. +comparison = parser.Results; +comparison.error = ''; +comparison.samplingA = []; +comparison.samplingB = []; +comparison.isGoodComparison = false; +comparison.subpixelsA = []; +comparison.subpixelsB = []; +comparison.normA = []; +comparison.normB = []; +comparison.normDiff = []; +comparison.absNormDiff = []; +comparison.relNormDiff = []; +comparison.corrcoef = []; +comparison.A = []; +comparison.B = []; +comparison.aMinusB = []; +comparison.bMinusA = []; + + +%% Load multispectral images. +dataA = load(renderingA.fileName); +dataB = load(renderingB.fileName); + +% do we have images? +hasImageA = isfield(dataA, 'multispectralImage'); +hasImageB = isfield(dataB, 'multispectralImage'); +if hasImageA + if hasImageB + % each has an image -- proceed + else + comparison.error = ... + 'Data file A has a multispectralImage but B does not.'; + disp(comparison.error); + return; + end +else + if hasImageB + comparison.error = ... + 'Data file B has a multispectralImage but A does not.'; + disp(comparison.error); + return; + else + comparison.error = ... + 'Neither data file A nor B has a multispectralImage.'; + disp(comparison.error); + return; + end +end +multispectralA = dataA.multispectralImage; +multispectralB = dataB.multispectralImage; + + +%% Sanity check image dimensions. +if ~isequal(size(multispectralA, 1), size(multispectralB, 1)) ... + || ~isequal(size(multispectralA, 2), size(multispectralB, 2)) + + comparison.error = ... + sprintf('Image A[%s] is not the same size as image B[%s].', ... + num2str(size(multispectralA)), num2str(size(multispectralB))); + disp(comparison.error); + return; +end + + +%% Sanity check spectral sampling. +hasSamplingA = isfield(dataA, 'S'); +hasSamplingB = isfield(dataB, 'S'); +if hasSamplingA + if hasSamplingB + % each has a sampling "S" -- proceed + else + comparison.error = ... + 'Data file A has a spectral sampling "S" but B does not.'; + disp(comparison.error); + return; + end +else + if hasSamplingB + comparison.error = ... + 'Data file B has a spectral sampling "S" but A does not.'; + disp(comparison.error); + return; + else + comparison.error = ... + 'Neither data file A nor B has spectral sampling "S".'; + return; + end +end +comparison.samplingA = dataA.S; +comparison.samplingB = dataB.S; + +% do spectral samplings agree? +if ~isequal(dataA.S, dataB.S) + comparison.error = ... + sprintf('Spectral sampling A[%s] is not the same as B[%s].', ... + num2str(dataA.S), num2str(dataB.S)); + disp(comparison.error); + % proceed with comparison, despite sampling mismatch +end + +% match images based on sampling depths +[multispectralA, multispectralB] = truncatePlanes( ... + multispectralA, multispectralB, dataA.S, dataB.S); + + +%% Sanity Checks OK. +comparison.isGoodComparison = true; + + +%% Per-pixel difference stats. +normA = multispectralA / max(multispectralA(:)); +normB = multispectralB / max(multispectralB(:)); +normDiff = normA - normB; +absNormDiff = abs(normDiff); +relNormDiff = absNormDiff ./ normA; +relNormDiff(normA < denominatorThreshold) = nan; + +% summarize differnece stats +comparison.subpixelsA = summarizeData(multispectralA); +comparison.subpixelsB = summarizeData(multispectralB); +comparison.normA = summarizeData(normA); +comparison.normB = summarizeData(normB); +comparison.normDiff = summarizeData(normDiff); +comparison.absNormDiff = summarizeData(absNormDiff); +comparison.relNormDiff = summarizeData(relNormDiff); + + +%% Overall correlation. +r = corrcoef(multispectralA(:), multispectralB(:)); +comparison.corrcoef = r(1, 2); + + +%% Difference images. +comparison.A = multispectralA; +comparison.B = multispectralB; +comparison.aMinusB = multispectralA - multispectralB; +comparison.bMinusA = multispectralB - multispectralA; + + +%% Truncate spectral planes if one image has more planes. +function [truncA, truncB, truncSampling] = truncatePlanes(A, B, samplingA, samplingB) +nPlanes = min(samplingA(3), samplingB(3)); +truncSampling = [samplingA(1:2) nPlanes]; +truncA = A(:,:,1:nPlanes); +truncB = B(:,:,1:nPlanes); + + +%% Summarize a distribuition of data with a struct of stats. +function summary = summarizeData(data) +finiteData = data(isfinite(data)); +summary.min = min(finiteData); +summary.mean = mean(finiteData); +summary.max = max(finiteData); diff --git a/Test/Interactive/Comparison/rtbFetchReferenceData.m b/Test/Interactive/Comparison/rtbFetchReferenceData.m new file mode 100644 index 0000000000000000000000000000000000000000..adca9695739b4a1568e18a11029764114c8b1172 --- /dev/null +++ b/Test/Interactive/Comparison/rtbFetchReferenceData.m @@ -0,0 +1,73 @@ +function [renderings, referenceRoot, artifact] = rtbFetchReferenceData(recipeName, varargin) +% Fetch a reference rendering and make it available locally. +% +% renderings = rtbFetchReferenceData(recipeName) fetches a reference data +% zip-file from the default brainard-archiva server. The given recipeName +% must be the name of an rtb example recipe, like 'rtbMakeDragon'. Expands +% the fetched zip file into the current directory. Returns a struct array +% of rendering data files that were found in the reference data. +% +% Also returns the path to the root folder where the zip file was expanded. +% Also returns the RemoteDataToolbox artifact record for the fetched data. +% +% rtbFetchReferenceData( ... 'rdtConfig', rdtConfig) specify the Remote +% Data Toolbox configuration to use. The default is 'render-toolbox'. +% +% rtbFetchReferenceData( ... 'remotePath', remotePath) specify the Remote +% Data Toolbox artifact path to use. The default is 'reference-data'. +% +% rtbFetchReferenceData( ... 'referenceVersion', referenceVersion) specify +% the Remote Data Toolbox artifact version to fetch. The default is '+', +% the latest available. +% +% rtbFetchReferenceData( ... 'referenceRoot', referenceRoot) specify the +% root folder where to expand the fetched zip file. The default is pwd(). +% +% [renderings, referenceRoot, artifact] = rtbFetchReferenceData(recipeName, varargin) +% +%%% RenderToolbox4 Copyright (c) 2012-2017 The RenderToolbox Team. +%%% About Us://github.com/RenderToolbox/RenderToolbox4/wiki/About-Us +%%% RenderToolbox4 is released under the MIT License. See LICENSE file. + +parser = inputParser(); +parser.KeepUnmatched = true; +parser.addRequired('recipeName', @ischar); +parser.addParameter('rdtConfig', 'render-toolbox'); +parser.addParameter('remotePath', 'reference-data', @ischar); +parser.addParameter('referenceVersion', '+', @ischar); +parser.addParameter('referenceRoot', pwd(), @ischar); +parser.parse(recipeName, varargin{:}); +recipeName = parser.Results.recipeName; +rdtConfig = parser.Results.rdtConfig; +remotePath = parser.Results.remotePath; +referenceVersion = parser.Results.referenceVersion; +referenceRoot = parser.Results.referenceRoot; + +%% Get a whole recipe from the server. +artifactPath = fullfile(remotePath, recipeName); +try + [fileName, artifact] = rdtReadArtifact(rdtConfig, artifactPath, recipeName, ... + 'version', referenceVersion, ... + 'type', 'zip'); +catch err + renderings = []; + artifact = []; + return; +end + +if isempty(fileName) + renderings = []; + artifact = []; + return; +end + + +%% Explode renderings it into the destination folder. +destination = fullfile(referenceRoot, recipeName); +if 7 ~= exist(destination, 'dir') + mkdir(destination); +end +unzip(fileName, destination); + +% scan for rendering records +renderings = rtbFindRenderings(destination); diff --git a/Test/Interactive/Comparison/rtbFindRenderings.m b/Test/Interactive/Comparison/rtbFindRenderings.m new file mode 100644 index 0000000000000000000000000000000000000000..274c18179ec0dea76d42a3512a2f63af0842d140 --- /dev/null +++ b/Test/Interactive/Comparison/rtbFindRenderings.m @@ -0,0 +1,94 @@ +function renderings = rtbFindRenderings(rootFolder, varargin) +%% Locate rendering data files in the given folder. +% +% renderings = rtbFindRenderings(rootFolder) scans the given rootFolder +% recursively for rendering data files. Returns a struct array of +% rendering records, one for each data file found. +% +% rtbFindRenderings( ... 'filter', filter) uses the given regular +% expression to filter the results. Only data files whose full paths match +% the expression will be compared. The default is '\.mat$', look for any +% .mat files. +% +% rtbFindRenderings( ... 'renderingsFolderName', renderingsFolderName) uses +% the given renderingsFolderName to locate renderings withing various +% subfolders of the given rootFolder. The default is "renderings", which +% is conisitent with the conventions established by rtbWorkingFolder() and +% rtbBatchRender(). +% +% Typical subfolders of rootFolder would look like these: +% rootFolder/rtbMakeMaterialSphereBumps/renderings/Mitsuba/materialSphereMetal.mat +% rootFolder/rtbMakeCoordinatesTest/renderings/PBRT/scene-003.mat +% +% These paths are parsed, starting with the renderingsFolderName, in this +% case "renderings". The folder above "renderings" is treated as the name +% of the example or recipe that produced the rendering. The folder below +% "renderings" is treated as the name of the renderer. The file name is +% scanned for either a name, like "materialSphereMetal", or a sequence +% number, like 3. +% +%%% RenderToolbox4 Copyright (c) 2012-2017 The RenderToolbox Team. +%%% About Us://github.com/RenderToolbox/RenderToolbox4/wiki/About-Us +%%% RenderToolbox4 is released under the MIT License. See LICENSE file. + +parser = inputParser(); +parser.KeepUnmatched = true; +parser.addRequired('rootFolder', @ischar); +parser.addParameter('filter', '\.mat$', @ischar); +parser.addParameter('renderingsFolderName', 'renderings', @ischar); +parser.parse(rootFolder, varargin{:}); +rootFolder = parser.Results.rootFolder; +filter = parser.Results.filter; +renderingsFolderName = parser.Results.renderingsFolderName; + +files = rtbFindFiles('root', rootFolder, 'filter', filter); +nFiles = numel(files); +renderingsCell = cell(1, nFiles); +for ff = 1:nFiles + fileName = files{ff}; + + % break off the file name + [parentPath, imageName] = fileparts(fileName); + if numel(imageName) > 3 + imageNumber = sscanf(imageName(end-3:end), '-%d'); + end + + % look for the renderingsFolderName, like "renderings" + scanResult = textscan(parentPath, '%s', 'Delimiter', filesep()); + pathParts = scanResult{1}; + nPathParts = numel(pathParts); + isRenderingsFolder = strcmp(pathParts, renderingsFolderName); + if ~any(isRenderingsFolder) + continue; + end + renderingsFolderIndex = find(isRenderingsFolder, 1, 'last'); + + % recipe name comes just before renderingsFolderName + recipeNameIndex = renderingsFolderIndex - 1; + recipeName = pathParts{recipeNameIndex}; + + % renderer name comes just after renderingsFolderName, if any + rendererNameIndex = renderingsFolderIndex + 1; + if nPathParts >= rendererNameIndex + rendererName = pathParts{rendererNameIndex}; + else + rendererName = ''; + end + + % the path leading up to the recipeNamem, if any, is where it came from + sourceFolderIndices = 1:(recipeNameIndex-1); + if isempty(sourceFolderIndices) + sourceFolder = ''; + else + sourceFolder = fullfile(pathParts{sourceFolderIndices}); + end + + renderingsCell{ff} = rtbRenderingRecord( ... + 'recipeName', recipeName, ... + 'rendererName', rendererName, ... + 'imageNumber', imageNumber, ... + 'imageName', imageName, ... + 'fileName', files{ff}, ... + 'sourceFolder', sourceFolder); +end +renderings = [renderingsCell{:}]; diff --git a/Test/Interactive/Comparison/rtbPlotManyRecipeComparisons.m b/Test/Interactive/Comparison/rtbPlotManyRecipeComparisons.m new file mode 100644 index 0000000000000000000000000000000000000000..72ef80c8d14779d9d8a13c8057b6acadc4073f32 --- /dev/null +++ b/Test/Interactive/Comparison/rtbPlotManyRecipeComparisons.m @@ -0,0 +1,134 @@ +function fig = rtbPlotManyRecipeComparisons(comparisons, varargin) +%% Plot a many recipe comparisons from rtbCompareManyRecipes(). +% +% fig = fig = rtbPlotManyRecipeComparisons(comparisons) makes a plot to +% visualize the given struct array of comparison results, as produced by +% rtbCompareManyRecipes(). +% +%%% RenderToolbox4 Copyright (c) 2012-2017 The RenderToolbox Team. +%%% About Us://github.com/RenderToolbox/RenderToolbox4/wiki/About-Us +%%% RenderToolbox4 is released under the MIT License. See LICENSE file. + +parser = inputParser(); +parser.KeepUnmatched = true; +parser.addRequired('comparisons', @isstruct); +parser.addParameter('fig', figure()); +parser.addParameter('figureWidth', 1000, @isnumeric); +parser.addParameter('nRows', 25, @isnumeric); +parser.addParameter('correlationMin', 0.8, @isnumeric); +parser.addParameter('correlationStep', 0.05, @isnumeric); +parser.addParameter('errorMax', 3, @isnumeric); +parser.addParameter('errorStep', 0.5, @isnumeric); +parser.parse(comparisons, varargin{:}); +comparisons = parser.Results.comparisons; +fig = parser.Results.fig; +figureWidth = parser.Results.figureWidth; +nRows = parser.Results.nRows; +correlationMin = parser.Results.correlationMin; +correlationStep = parser.Results.correlationStep; +errorMax = parser.Results.errorMax; +errorStep = parser.Results.errorStep; + + +%% Set up the figure. +figureName = sprintf('Summary of %d rendering comparisons', ... + numel(comparisons)); +set(fig, ... + 'Name', figureName, ... + 'NumberTitle', 'off'); + +position = get(fig, 'Position'); +if position(3) < figureWidth + position(3) = figureWidth; + set(fig, 'Position', position); +end + +%% Summary of correlation coefficients. +correlationTicks = correlationMin : correlationStep : 1; +correlationTickLabels = num2cell(correlationTicks); +correlationTickLabels{1} = '<='; +correlation = [comparisons.corrcoef]; +correlation(correlation < correlationMin) = correlationMin; + +renderingsA = [comparisons.renderingA]; +names = {renderingsA.identifier}; +nLines = numel(names); +ax(1) = subplot(1, 3, 2, ... + 'Parent', fig, ... + 'YTick', 1:nLines, ... + 'YTickLabel', names, ... + 'YGrid', 'on', ... + 'XLim', [correlationTicks(1), correlationTicks(end)], ... + 'XTick', correlationTicks, ... + 'XTickLabel', correlationTickLabels); +line(correlation, 1:nLines, ... + 'Parent', ax(1), ... + 'LineStyle', 'none', ... + 'Marker', 'o', ... + 'Color', [0 0 1]) +xlabel(ax(1), 'correlation'); + + +%% Overall title. +name = sprintf('%s vs %s', ... + comparisons(1).renderingA.sourceFolder, ... + comparisons(1).renderingB.sourceFolder); +title(ax(1), name, 'Interpreter', 'none'); + + +%% Summary of mean and max subpixel differences. +errorTicks = 0 : errorStep : errorMax; +errorTickLabels = num2cell(errorTicks); +errorTickLabels{end} = '>='; + +relNormDiff = [comparisons.relNormDiff]; +maxes = [relNormDiff.max]; +means = [relNormDiff.mean]; +maxes(maxes > errorMax) = errorMax; +means(means > errorMax) = errorMax; +ax(2) = subplot(1, 3, 3, ... + 'Parent', fig, ... + 'YTick', 1:nLines, ... + 'YTickLabel', 1:nLines, ... + 'YAxisLocation', 'right', ... + 'YGrid', 'on', ... + 'XLim', [errorTicks(1), errorTicks(end)], ... + 'XTick', errorTicks, ... + 'XTickLabel', errorTickLabels); +line(maxes, 1:nLines, ... + 'Parent', ax(2), ... + 'LineStyle', 'none', ... + 'Marker', '+', ... + 'Color', [1 0 0]) +line(means, 1:nLines, ... + 'Parent', ax(2), ... + 'LineStyle', 'none', ... + 'Marker', 'o', ... + 'Color', [0 0 0]) +legend(ax(2), 'max', 'mean', 'Location', 'northeast'); +xlabel(ax(2), 'relative diff'); + + +%% Let the user scroll both axes at the same time. +scrollerData.axes = ax; +scrollerData.nLinesAtATime = nRows; +scroller = uicontrol( ... + 'Parent', fig, ... + 'Units', 'normalized', ... + 'Position', [.95 0 .05 1], ... + 'Callback', @scrollAxes, ... + 'Min', 1, ... + 'Max', max(2, nLines), ... + 'Value', nLines, ... + 'Style', 'slider', ... + 'SliderStep', [1 2], ... + 'UserData', scrollerData); +scrollAxes(scroller, []); + + +%% Scroll summary axes together. +function scrollAxes(object, event) +scrollerData = get(object, 'UserData'); +topLine = get(object, 'Value'); +yLimit = topLine + [-scrollerData.nLinesAtATime 1]; +set(scrollerData.axes, 'YLim', yLimit); diff --git a/Test/Interactive/Comparison/rtbPlotRenderingComparison.m b/Test/Interactive/Comparison/rtbPlotRenderingComparison.m new file mode 100644 index 0000000000000000000000000000000000000000..185bb1e0d2d19a6223f5ecdb180f4d8c78b38a75 --- /dev/null +++ b/Test/Interactive/Comparison/rtbPlotRenderingComparison.m @@ -0,0 +1,64 @@ +function fig = rtbPlotRenderingComparison(comparison, varargin) +%% Plot a rendering comparison from rtbCompareRenderings(). +% +% fig = rtbPlotRenderingComparison(comparison) makes a plot to visualize +% the given struct of comparison results, as produced by +% rtbCompareRenderings(). +% +%%% RenderToolbox4 Copyright (c) 2012-2017 The RenderToolbox Team. +%%% About Us://github.com/RenderToolbox/RenderToolbox4/wiki/About-Us +%%% RenderToolbox4 is released under the MIT License. See LICENSE file. + +parser = inputParser(); +parser.KeepUnmatched = true; +parser.addRequired('comparison', @isstruct); +parser.addParameter('isScale', true, @islogical); +parser.addParameter('toneMapFactor', 0, @isnumeric); +parser.addParameter('fig', figure()); +parser.parse(comparison, varargin{:}); +comparison = parser.Results.comparison; +isScale = parser.Results.isScale; +toneMapFactor = parser.Results.toneMapFactor; +fig = parser.Results.fig; + + +%% Compute RGB images for viewing. +S = comparison.samplingA; +rgbA = rtbMultispectralToSRGB(comparison.A, S, ... + 'toneMapFactor', toneMapFactor, 'isScale', isScale); +rgbB = rtbMultispectralToSRGB(comparison.B, S, ... + 'toneMapFactor', toneMapFactor, 'isScale', isScale); +rgbAminusB = rtbMultispectralToSRGB(comparison.aMinusB, S, ... + 'toneMapFactor', toneMapFactor, 'isScale', isScale); +rgbBminusA = rtbMultispectralToSRGB(comparison.bMinusA, S, ... + 'toneMapFactor', toneMapFactor, 'isScale', isScale); + + +%% Make the plot. +name = sprintf('%s isScale %d toneMapFactor %.2f', ... + comparison.renderingA.identifier, ... + isScale, ... + toneMapFactor); +set(fig, ... + 'Name', name, ... + 'NumberTitle', 'off'); + +ax = subplot(2, 2, 2, 'Parent', fig); +imshow(uint8(rgbA), 'Parent', ax); +title(ax, 'A'); +xlabel(ax, comparison.renderingA.sourceFolder, ... + 'Interpreter', 'none'); + +ax = subplot(2, 2, 3, 'Parent', fig); +imshow(uint8(rgbB), 'Parent', ax); +title(ax, 'B'); +xlabel(ax, comparison.renderingB.sourceFolder, ... + 'Interpreter', 'none'); + +ax = subplot(2, 2, 1, 'Parent', fig); +imshow(uint8(rgbAminusB), 'Parent', ax); +title(ax, 'A - B'); + +ax = subplot(2, 2, 4, 'Parent', fig); +imshow(uint8(rgbBminusA), 'Parent', ax); +title(ax, 'B - A'); diff --git a/Test/Interactive/Comparison/rtbRenderingRecord.m b/Test/Interactive/Comparison/rtbRenderingRecord.m new file mode 100644 index 0000000000000000000000000000000000000000..db000e3dc45b2db96ad0e1eb4d93e7a6ee8e8863 --- /dev/null +++ b/Test/Interactive/Comparison/rtbRenderingRecord.m @@ -0,0 +1,49 @@ +function record = rtbRenderingRecord(varargin) +% Make a well-formed struct to represent a rendering data file. +% +% The idea is to represent a rendering result using a consistent +% struct format. This will make it easier to compare sets of renderings +% because we can compare rendering records for equality without worrying +% about fussy syntax details of file names and paths. +% +% record = rtbRenderingRecord() creates a placeholder record with the +% correct fields. +% +% record = rtbRenderingRecord( ... name, value) fills in the record with +% fields based on the given names-value pairs. Unrecognized names +% will be ignored. +% +%%% RenderToolbox4 Copyright (c) 2012-2017 The RenderToolbox Team. +%%% About Us://github.com/RenderToolbox/RenderToolbox4/wiki/About-Us +%%% RenderToolbox4 is released under the MIT License. See LICENSE file. + +parser = inputParser(); +parser.KeepUnmatched = true; +parser.addParameter('recipeName', '', @ischar); +parser.addParameter('rendererName', '', @ischar); +parser.addParameter('imageNumber', [], @isnumeric); +parser.addParameter('imageName', '',@ischar); +parser.addParameter('fileName', '', @ischar); +parser.addParameter('sourceFolder', '', @ischar); +parser.parse(varargin{:}); + +% let the parser do most of the work +record = parser.Results; + +% format an identifier useful for comparing records with eg setdiff() +recipeName = record.recipeName; +if strncmp(recipeName, 'rtb', 3) + recipeName = recipeName(4:end); +end + +if isempty(record.imageNumber) + record.identifier = sprintf('%s-%s-%s', ... + recipeName, ... + record.rendererName, ... + record.imageName); +else + record.identifier = sprintf('%s-%s-%d', ... + recipeName, ... + record.rendererName, ... + record.imageNumber); +end diff --git a/Test/Interactive/Comparison/rtbRunEpicComparison.m b/Test/Interactive/Comparison/rtbRunEpicComparison.m new file mode 100644 index 0000000000000000000000000000000000000000..5ea42c274ae7a849ec309c9de81cfe21a8fb674c --- /dev/null +++ b/Test/Interactive/Comparison/rtbRunEpicComparison.m @@ -0,0 +1,145 @@ +function [comparisons, matchInfo, figs] = rtbRunEpicComparison(folderA, folderB, varargin) +%% Compare sets of renderings for similarity. +% +% comparisons = rtbRunEpicComparison(folderA, folderB) locates renderings +% in folderA and folderB, compares pairs of renderings found between the +% two folders, and plots a summary of the comparisons. Returns a struct +% array of comparison results, one for each pair of renderings. +% +% Also returns a struct array of info about how renderings were matched +% between folderA and folderB, including renderings from each folder that +% were unmatched. +% +% Also returns an array of figure handles for visualizations of the +% comparison results. +% +% rtbRunEpicComparison( ... 'plotSummary', plotSummary) whether to create a +% plot summarizing the overall comparison results. The default is true, +% make a summary plot. +% +% rtbRunEpicComparison( ... 'closeSummary', closeSummary) whether to create +% a close the overalls summary plot when done. This is useful when you +% specify a figureFolder, where the summary plot can be saved to disk. the +% default is false, don't close the summary figure. +% +% rtbRunEpicComparison( ... 'plotImages', plotImages) whether to create a +% plot showing images and difference images for each pair of renderings. +% The default is false, don't show the images for each pair. +% +% rtbRunEpicComparison( ... 'closeImages', closeImages) whether to close +% the image plot for each pair of renderings when done. This is useful +% when you specify a figureFolder, where the images can be saved to disk. +% The default is true, do close the image figures. +% +% rtbRunEpicComparison( ... 'figureFolder', figureFolder) specify a folder +% where generated plots should be saved to disk, as a .fig file and as a +% .png file. The default is '', don't save figures to disk. +% +%%% RenderToolbox4 Copyright (c) 2012-2017 The RenderToolbox Team. +%%% About Us://github.com/RenderToolbox/RenderToolbox4/wiki/About-Us +%%% RenderToolbox4 is released under the MIT License. See LICENSE file. + +parser = inputParser(); +parser.addRequired('folderA', @ischar); +parser.addRequired('folderB', @ischar); +parser.addParameter('plotSummary', true, @islogical); +parser.addParameter('closeSummary', false, @islogical); +parser.addParameter('plotImages', true, @islogical); +parser.addParameter('closeImages', false, @islogical); +parser.addParameter('figureFolder', '', @ischar); +parser.addParameter('summaryName', 'epic-summary', @ischar); +parser.parse(folderA, folderB, varargin{:}); +folderA = parser.Results.folderA; +folderB = parser.Results.folderB; +plotSummary = parser.Results.plotSummary; +closeSummary = parser.Results.closeSummary; +plotImages = parser.Results.plotImages; +closeImages = parser.Results.closeImages; +figureFolder = parser.Results.figureFolder; +summaryName = parser.Results.summaryName; + +figs = []; + + +%% Run the grand comparison. +[comparisons, matchInfo] = rtbCompareManyRecipes(folderA, folderB, ... + varargin{:}); + + +%% Sort the summary by size of error. +goodComparisons = comparisons([comparisons.isGoodComparison]); +relNormDiff = [goodComparisons.relNormDiff]; +errorStat = [relNormDiff.max]; +[~, order] = sort(errorStat); +goodComparisons = goodComparisons(order); + + +%% Plot the summary. +if plotSummary + summaryFig = rtbPlotManyRecipeComparisons(goodComparisons, ... + varargin{:}); + + if ~isempty(figureFolder); + imageFileName = fullfile(figureFolder, summaryName); + saveFigure(summaryFig, imageFileName); + end + + if closeSummary + close(summaryFig); + else + figs = [figs summaryFig]; + end +end + + +%% Plot the detail images for each rendering. +if plotImages + nComparisons = numel(goodComparisons); + imageFigs = cell(1, nComparisons); + for cc = 1:nComparisons + imageFig = rtbPlotRenderingComparison(goodComparisons(cc), ... + varargin{:}); + + if ~isempty(figureFolder); + identifier = goodComparisons(cc).renderingA.identifier; + imageFileName = fullfile(figureFolder, identifier); + saveFigure(imageFig, imageFileName); + end + + if closeImages + close(imageFig); + else + imageFigs{cc} = imageFig; + end + end + figs = [figs imageFigs{:}]; +end + + +%% Save a figure to file, watch out for things like uicontrols. +function saveFigure(fig, fileName) + +% flush pending drawing commands +drawnow(); + +% hide uicontrols, which can't always be saved +controls = findobj(fig, 'Type', 'uicontrol'); +set(controls, 'Visible', 'off'); + +% make sure output location exists +[filePath, fileName] = fileparts(fileName); +if 7 ~= exist(filePath, 'dir') + mkdir(filePath); +end + +% save a png and a figure +figName = fullfile(filePath, [fileName '.fig']); +saveas(fig, figName, 'fig'); + +pngName = fullfile(filePath, [fileName '.png']); +set(fig, 'PaperPositionMode', 'auto'); +saveas(fig, pngName, 'png'); + +% restore uicontrols +set(controls, 'Visible', 'on'); + diff --git a/Test/Interactive/rtbCompareAllExampleScenes.m b/Test/Interactive/rtbCompareAllExampleScenes.m deleted file mode 100644 index e0ec06ba9f59256a80f9b0c5fef489b34c1ec45c..0000000000000000000000000000000000000000 --- a/Test/Interactive/rtbCompareAllExampleScenes.m +++ /dev/null @@ -1,565 +0,0 @@ -function [matchInfo, unmatchedA, unmatchedB] = rtbCompareAllExampleScenes(workingFolderA, workingFolderB, varargin) -%% Compare recipe renderings that were generated at different times. -% -% [matchInfo, unmatchedA, unmatchedB] = rtbCompareAllExampleScenes(workingFolderA, workingFolderB) -% Finds 2 sets of rendering outputs: set A includes renderings located -% under the given workingFolderA, set B includes renderings located -% under workingFolderB. Attempts to match up data files from both sets, -% based on recipe names and renderer names. Computes comparison statistics -% and shows difference images for each matched pair. -% -% Data sets must use a particular folder structure, consistent with -% rtbWorkingFolder(). For each rendering data file, the expected path is: -% workingFolder/recipeName/rendererName/renderings/fileName.mat -% -% where: -% - workingFolder is either workingFolderA or workingFolderB -% - recipeName must be the name of a recipe such as "MakeDragon" -% - rendererName must be the name of a renderer, like "PBRT" or "Mitsuba" -% - fileName must be the name of a multi-spectral data file, such as "Dragon-001" -% -% rtbCompareAllExampleScenes( ... 'filterExpression', filterExpression) -% uses the given regular expression filterExpr'/home/ben/Desktop/testA'ession to select file paths. -% Only data files whose paths match the expression will be compared. The -% default is to do no such filtering. -% -% rtbCompareAllExampleScenes( ... 'visualize', visualize) specifies the -% level of visualization to do during comparinsons. The options are: -% - 0 -- don't plot anything -% - 1 -- (default) plot a summary figure at the end -% - 2 -- plot a summary figure at the end and a detail figure for each comparison -% -% The summary figure will contain two plots: -% -% A "correlation" plot will show this correlation betweeen paired -% multi-spectral images, with each image simply treated as a matrix of -% numbers. -% -% A "relative diff" plot will show the relative differences between paired -% pixel components, where raw diff is -% diff = |a-b|/a, where a/max(a) > .2 -% The diff is only calculated when the "a" value is not small, to avoid -% unfair comparison due to large denominator. For each pair of images, the -% plot will show the mean and max of the diffs from all pixel components. -% -% rtbCompareAllExampleScenes( ... 'figureFolder', figureFolder) specifies -% an output folder where to save figures used for visualization. The -% default is rtbWorkingFolder(). -% -% This function is intended to help validate RenderToolbox installations -% and detect bugs in the RenderToolbox code. A potential use would -% compare renderings produced locally with archived renderings located at -% Amazon S3. For example: -% % produce renderings locally -% rtbTestAllExampleScenes('my/local/renderings'); -% -% % download archived renderings to 'my/local/archive' -% -% % summarize local vs archived renderings -% workingFolderA = 'my/local/renderings/data'; -% workingFolderA = 'my/local/archive/data'; -% visualize = 1; -% matchInfo = rtbCompareAllExampleScenes(workingFolderA, workingFolderB, 'visualize', visualize); -% -% Returns a struct array of info about each matched pair, including file -% names and differneces between multispectral images (A minus B). -% -% Also returns a cell array of paths for files in set A that did not match -% any of the files in set B. Likewise, returns a cell array of paths for -% files in set B that did not match any of the files in set A. -% -%%% RenderToolbox4 Copyright (c) 2012-2016 The RenderToolbox Team. -%%% About Us://github.com/RenderToolbox/RenderToolbox4/wiki/About-Us -%%% RenderToolbox4 is released under the MIT License. See LICENSE file. - -parser = inputParser(); -parser.addRequired('workingFolderA', @ischar); -parser.addRequired('workingFolderB', @ischar); -parser.addParameter('filterExpression', '', @ischar); -parser.addParameter('visualize', 1, @isnumeric); -parser.addParameter('figureFolder', fullfile(rtbWorkingFolder(), 'comparisons'), @ischar); -parser.parse(workingFolderA, workingFolderB, varargin{:}); -workingFolderA = parser.Results.workingFolderA; -workingFolderB = parser.Results.workingFolderB; -filterExpression = parser.Results.filterExpression; -visualize = parser.Results.visualize; -figureFolder = parser.Results.figureFolder; - -matchInfo = []; -unmatchedA = {}; -unmatchedB = {}; - -% find .mat files for sets A and B -fileFilter = [filterExpression '[^\.]*\.mat$']; -filesA = rtbFindFiles('root', workingFolderA, 'filter', fileFilter); -filesB = rtbFindFiles('root', workingFolderB, 'filter', fileFilter); - -if isempty(filesA) - fprintf('Found no files for set A in: %s\n', workingFolderA); - return; -end - -if isempty(filesB) - fprintf('Found no files for set B in: %s\n', workingFolderB); - return; -end - -% parse out expected path parts for each file -infoA = scanDataPaths(filesA); -infoB = scanDataPaths(filesB); - -% report unmatched files -matchTokensA = {infoA.matchToken}; -matchTokensB = {infoB.matchToken}; -[~, indexA, indexB] = intersect( ... - matchTokensA, matchTokensB, 'stable'); -[~, unmatchedIndex] = setdiff(matchTokensA, matchTokensB); -unmatchedA = filesA(unmatchedIndex); -[~, unmatchedIndex] = setdiff(matchTokensB, matchTokensA); -unmatchedB = filesB(unmatchedIndex); - -if isempty(indexA) || isempty(indexB) - fprintf('Could not find any file matches.\n'); - return; -end - -% allocate an info struct for image comparisons -filesA = {infoA.original}; -filesB = {infoB.original}; -matchInfo = struct( ... - 'fileA', filesA(indexA), ... - 'fileB', filesB(indexB), ... - 'workingFolderA', workingFolderA, ... - 'workingFolderB', workingFolderB, ... - 'relativePathA', {infoA(indexA).relativePath}, ... - 'relativePathB', {infoB(indexB).relativePath}, ... - 'matchTokenA', matchTokensA(indexA), ... - 'matchTokenB', matchTokensB(indexB), ... - 'samplingA', [], ... - 'samplingB', [], ... - 'denominatorThreshold', 0.2, ... - 'subpixelsA', [], ... - 'subpixelsB', [], ... - 'normA', [], ... - 'normB', [], ... - 'normDiff', [], ... - 'absNormDiff', [], ... - 'relNormDiff', [], ... - 'corrcoef', nan, ... - 'isGoodComparison', false, ... - 'detailFigure', nan, ... - 'error', ''); - -% any comparisons to make? -nMatches = numel(matchInfo); -if nMatches > 0 - fprintf('Found %d matched pairs of data files.\n', nMatches); - fprintf('Some of these might not contain images and would be skipped.\n'); -else - fprintf('Found no matched pairs.\n'); - return; -end -fprintf('\n') - -nUnmatchedA = numel(unmatchedA); -if nUnmatchedA > 0 - fprintf('%d data files in set A had no match in set B:\n', nUnmatchedA); - for ii = 1:nUnmatchedA - fprintf(' %s\n', unmatchedA{ii}); - end -end -fprintf('\n') - -nUnmatchedB = numel(unmatchedB); -if nUnmatchedB > 0 - fprintf('%d data files in set B had no match in set A:\n', nUnmatchedB); - for ii = 1:nUnmatchedB - fprintf(' %s\n', unmatchedB{ii}); - end -end -fprintf('\n') - -% compare matched images! -for ii = 1:nMatches - fprintf('%d of %d: %s\n', ii, nMatches, matchInfo(ii).matchTokenA); - - % load data - dataA = load(matchInfo(ii).fileA); - dataB = load(matchInfo(ii).fileB); - - % do we have images? - hasImageA = isfield(dataA, 'multispectralImage'); - hasImageB = isfield(dataB, 'multispectralImage'); - if hasImageA - if hasImageB - % each has an image -- proceed - else - matchInfo(ii).error = ... - 'Data file A has a multispectralImage but B does not!'; - disp(matchInfo(ii).error); - continue; - end - else - if hasImageB - matchInfo(ii).error = ... - 'Data file B has a multispectralImage but A does not!'; - disp(matchInfo(ii).error); - continue; - else - matchInfo(ii).error = ... - 'Neither data file A nor B has a multispectralImage -- skipping.'; - continue; - end - end - multispectralA = dataA.multispectralImage; - multispectralB = dataB.multispectralImage; - - % do image dimensions agree? - if ~isequal(size(multispectralA, 1), size(multispectralB, 1)) ... - || ~isequal(size(multispectralA, 2), size(multispectralB, 2)) - matchInfo(ii).error = ... - sprintf('Image A[%s] is not the same size as image B[%s].', ... - num2str(size(multispectralA)), num2str(size(multispectralB))); - disp(matchInfo(ii).error); - continue; - end - - % do we have spectral sampling? - hasSamplingA = isfield(dataA, 'S'); - hasSamplingB = isfield(dataB, 'S'); - if hasSamplingA - if hasSamplingB - % each has a sampling "S" -- proceed - else - matchInfo(ii).error = ... - 'Data file A has a spectral sampling "S" but B does not!'; - disp(matchInfo(ii).error); - continue; - end - else - if hasSamplingB - matchInfo(ii).error = ... - 'Data file B has a spectral sampling "S" but A does not!'; - disp(matchInfo(ii).error); - continue; - else - matchInfo(ii).error = ... - 'Neither data file A nor B has spectral sampling "S" -- skipping.'; - continue; - end - end - matchInfo(ii).samplingA = dataA.S; - matchInfo(ii).samplingB = dataB.S; - - % do spectral samplings agree? - if ~isequal(dataA.S, dataB.S) - matchInfo(ii).error = ... - sprintf('Spectral sampling A[%s] is not the same as B[%s].', ... - num2str(dataA.S), num2str(dataB.S)); - disp(matchInfo(ii).error); - % proceed with comparison, despite sampling mismatch - end - - % tolerate different sectral sampling depths - [A, B] = truncatePlanes(multispectralA, multispectralB, dataA.S, dataB.S); - - % comparison passes all sanity checks - matchInfo(ii).isGoodComparison = true; - - % compute per-pixel component difference stats - normA = A / max(A(:)); - normB = B / max(B(:)); - normDiff = normA - normB; - absNormDiff = abs(normDiff); - relNormDiff = absNormDiff ./ normA; - cutoff = matchInfo(ii).denominatorThreshold; - relNormDiff(normA < cutoff) = nan; - - % summarize differnece stats - matchInfo(ii).subpixelsA = summarizeData(A); - matchInfo(ii).subpixelsB = summarizeData(B); - matchInfo(ii).normA = summarizeData(normA); - matchInfo(ii).normB = summarizeData(normB); - matchInfo(ii).normDiff = summarizeData(normDiff); - matchInfo(ii).absNormDiff = summarizeData(absNormDiff); - matchInfo(ii).relNormDiff = summarizeData(relNormDiff); - - % compute correlation among pixel components - r = corrcoef(A(:), B(:)); - matchInfo(ii).corrcoef = r(1, 2); - - % plot difference image? - if visualize > 1 - f = showDifferenceImage(matchInfo(ii), A, B); - matchInfo(ii).detailFigure = f; - - % save detail figure to disk - drawnow(); - [imagePath, imageName] = fileparts(matchInfo(ii).relativePathA); - imageCompPath = fullfile(figureFolder, imagePath); - if ~exist(imageCompPath, 'dir') - mkdir(imageCompPath); - end - figName = fullfile(imageCompPath, [imageName '.fig']); - saveas(f, figName, 'fig'); - pngName = fullfile(imageCompPath, [imageName '.png']); - set(f, 'PaperPositionMode', 'auto'); - saveas(f, pngName, 'png'); - - close(f); - end -end - -nComparisons = sum([matchInfo.isGoodComparison]); -nSkipped = nMatches - nComparisons; -fprintf('Compared %d pairs of data files.\n', nComparisons); -fprintf('Skipped %d pairs of data files, which is not necessarily a problem.\n', nSkipped); - -% plot a grand summary? -if visualize > 0 - f = showDifferenceSummary(matchInfo); - - % save summary figure to disk - if ~exist(figureFolder, 'dir') - mkdir(figureFolder); - end - imageName = sprintf('%s-summary', mfilename()); - figName = fullfile(figureFolder, [imageName '.fig']); - saveas(f, figName, 'fig'); - - % some platforms can't pring uicontrols - controls = findobj(f, 'Type', 'uicontrol'); - set(controls, 'Visible', 'off'); - pngName = fullfile(figureFolder, [imageName '.png']); - saveas(f, pngName, 'png'); - set(controls, 'Visible', 'on'); -end - -if visualize > 1 - fprintf('\nSee comparison images saved in:\n %s\n', figureFolder); -end - - -% Scan paths for expected parts: -% root/recipeName/subfolderName/rendererName/fileName.extension -function info = scanDataPaths(paths) -n = numel(paths); -rootPath = cell(1, n); -relativePath = cell(1, n); -recipeName = cell(1, n); -subfolderName = cell(1, n); -rendererName = cell(1, n); -fileName = cell(1, n); -fileNumber = cell(1, n); -hasNumber = false(1, n); -matchToken = cell(1, n); -for ii = 1:n - % break off the file name - [parentPath, baseName, extension] = fileparts(paths{ii}); - fileName{ii} = [baseName extension]; - if numel(baseName) >= 4 - fileNumber{ii} = sscanf(baseName(end-3:end), '-%d'); - end - hasNumber(ii) = ~isempty(fileNumber{ii}); - - % break out subfolder names - scanResult = textscan(parentPath, '%s', 'Delimiter', filesep()); - tokens = scanResult{1}; - - % is there a renderer folder? - if any(strcmp(tokens{end}, {'PBRT', 'Mitsuba'})) - rendererName{ii} = tokens{end}; - subfolderNameIndex = numel(tokens) - 1; - else - rendererName{ii} = ''; - subfolderNameIndex = numel(tokens); - end - - % get the named subfolder name - subfolderName{ii} = tokens{subfolderNameIndex}; - - % get the recipe name - recipeName{ii} = tokens{subfolderNameIndex-1}; - - % get the root path - rootPath{ii} = fullfile(tokens{1:subfolderNameIndex-2}); - - % build the rootless relative path - relativePath{ii} = fullfile(recipeName{ii}, subfolderName{ii}, ... - rendererName{ii}, fileName{ii}); - - % build a token for matching across file sets - if strncmp(recipeName{ii}, 'rtb', 3) - nameBase = recipeName{ii}(4:end); - else - nameBase = recipeName{ii}; - end - matchTokenBase = [nameBase '-' subfolderName{ii} '-' rendererName{ii} '-']; - if hasNumber(ii) - matchToken{ii} = [matchTokenBase sprintf('%03d', fileNumber{ii})]; - else - matchToken{ii} = [matchTokenBase baseName]; - end -end - -info = struct( ... - 'original', paths, ... - 'fileName', fileName, ... - 'fileNumber', fileNumber, ... - 'hasNumber', hasNumber, ... - 'rendererName', rendererName, ... - 'recipeName', recipeName, ... - 'subfolderName', subfolderName, ... - 'rootPath', rootPath, ... - 'relativePath', relativePath, ... - 'matchToken', matchToken); - - -% Show sRGB images and sRGB difference images -function f = showDifferenceImage(info, A, B) - -% make SRGB images -[A, B, S] = truncatePlanes(A, B, info.samplingA, info.samplingB); -isScale = true; -toneMapFactor = 0; -imageA = rtbMultispectralToSRGB(A, S, 'toneMapFactor', toneMapFactor, 'isScale', isScale); -imageB = rtbMultispectralToSRGB(B, S, 'toneMapFactor', toneMapFactor, 'isScale', isScale); -imageAB = rtbMultispectralToSRGB(A-B, S, 'toneMapFactor', toneMapFactor, 'isScale', isScale); -imageBA = rtbMultispectralToSRGB(B-A, S, 'toneMapFactor', toneMapFactor, 'isScale', isScale); - -% show images in a new figure -name = sprintf('sRGB scaled: %s', info.matchTokenA); -f = figure('Name', name, 'NumberTitle', 'off'); - -ax = subplot(2, 2, 2, 'Parent', f); -imshow(uint8(imageA), 'Parent', ax); -title(ax, ['A: ' info.workingFolderA]); - -ax = subplot(2, 2, 3, 'Parent', f); -imshow(uint8(imageB), 'Parent', ax); -title(ax, ['B: ' info.workingFolderB]); - -ax = subplot(2, 2, 1, 'Parent', f); -imshow(uint8(imageAB), 'Parent', ax); -title(ax, 'Difference: A - B'); - -ax = subplot(2, 2, 4, 'Parent', f); -imshow(uint8(imageBA), 'Parent', ax); -title(ax, 'Difference: B - A'); - - -% Truncate spectral planes if one image has more planes. -function [truncA, truncB, truncSampling] = truncatePlanes(A, B, samplingA, samplingB) -nPlanes = min(samplingA(3), samplingB(3)); -truncSampling = [samplingA(1:2) nPlanes]; -truncA = A(:,:,1:nPlanes); -truncB = B(:,:,1:nPlanes); - - -% Show a summary of all difference images. -function f = showDifferenceSummary(info) -figureName = sprintf('A: %s vs B: %s', ... - info(1).workingFolderA, info(1).workingFolderB); -f = figure('Name', figureName, 'NumberTitle', 'off'); - -% summarize only fair comparisions -goodInfo = info([info.isGoodComparison]); - -% sort the summary by size of error -diffSummary = [goodInfo.relNormDiff]; -errorStat = [diffSummary.max]; -[~, order] = sort(errorStat); -goodInfo = goodInfo(order); - -% summarize data correlation coefficients -minCorr = 0.85; -peggedCorr = 0.8; -corrTicks = [peggedCorr minCorr:0.05:1]; -corrTickLabels = num2cell(corrTicks); -corrTickLabels{1} = sprintf('<%.2f', minCorr); -corr = [goodInfo.corrcoef]; -corr(corr < minCorr) = peggedCorr; - -names = {goodInfo.matchTokenA}; -nLines = numel(names); -ax(1) = subplot(1, 3, 2, ... - 'Parent', f, ... - 'YTick', 1:nLines, ... - 'YTickLabel', names, ... - 'YGrid', 'on', ... - 'XLim', [corrTicks(1), corrTicks(end)], ... - 'XTick', corrTicks, ... - 'XTickLabel', corrTickLabels); -line(corr, 1:nLines, ... - 'Parent', ax(1), ... - 'LineStyle', 'none', ... - 'Marker', 'o', ... - 'Color', [0 0 1]) -title(ax(1), 'correlation'); - -% summarize mean and max subpixel differences -maxDiff = 2.5; -peggedDiff = 3; -diffTicks = [0:0.5:maxDiff peggedDiff]; -diffTickLabels = num2cell(diffTicks); -diffTickLabels{end} = sprintf('>%.2f', maxDiff); - -diffSummary = [goodInfo.relNormDiff]; -maxes = [diffSummary.max]; -means = [diffSummary.mean]; -maxes(maxes > maxDiff) = peggedDiff; -means(means > maxDiff) = peggedDiff; -ax(2) = subplot(1, 3, 3, ... - 'Parent', f, ... - 'YTick', 1:nLines, ... - 'YTickLabel', 1:nLines, ... - 'YAxisLocation', 'right', ... - 'YGrid', 'on', ... - 'XLim', [diffTicks(1), diffTicks(end)], ... - 'XTick', diffTicks, ... - 'XTickLabel', diffTickLabels); -line(maxes, 1:nLines, ... - 'Parent', ax(2), ... - 'LineStyle', 'none', ... - 'Marker', '+', ... - 'Color', [1 0 0]) -line(means, 1:nLines, ... - 'Parent', ax(2), ... - 'LineStyle', 'none', ... - 'Marker', 'o', ... - 'Color', [0 0 0]) -legend(ax(2), 'max', 'mean', 'Location', 'northeast'); -title(ax(2), 'relative diff'); - -% let the user scroll both axes at the same time -nLinesAtATime = 25; -scrollerData.axes = ax; -scrollerData.nLinesAtATime = nLinesAtATime; -scroller = uicontrol( ... - 'Parent', f, ... - 'Units', 'normalized', ... - 'Position', [.95 0 .05 1], ... - 'Callback', @scrollSummaryAxes, ... - 'Min', 1, ... - 'Max', max(2, nLines), ... - 'Value', nLines, ... - 'Style', 'slider', ... - 'SliderStep', [1 2], ... - 'UserData', scrollerData); -scrollSummaryAxes(scroller, []); - - -% Summarize a distribuition of data with a struct of stats. -function summary = summarizeData(data) -finiteData = data(isfinite(data)); -summary.min = min(finiteData); -summary.mean = mean(finiteData); -summary.max = max(finiteData); - - -% Scroll summary axes together. -function scrollSummaryAxes(object, event) -scrollerData = get(object, 'UserData'); -topLine = get(object, 'Value'); -yLimit = topLine + [-scrollerData.nLinesAtATime 1]; -set(scrollerData.axes, 'YLim', yLimit); diff --git a/Test/Interactive/rtbTestAllExampleScenes.m b/Test/Interactive/rtbRunEpicTest.m similarity index 93% rename from Test/Interactive/rtbTestAllExampleScenes.m rename to Test/Interactive/rtbRunEpicTest.m index 12d549dfa9c7cc2b865d4f09a2e245f3cd73d438..68ba5ea8997032c0ed9af5247eb92f5e11dadcc3 100644 --- a/Test/Interactive/rtbTestAllExampleScenes.m +++ b/Test/Interactive/rtbRunEpicTest.m @@ -1,7 +1,7 @@ -function results = rtbTestAllExampleScenes(varargin) +function results = rtbRunEpicTest(varargin) %% Run all "rtbMake..." scripts in the ExampleScenes/ folder. % -% results = rtbTestAllExampleScenes() renders example scenes by invoking +% results = rtbRunEpicTest() renders example scenes by invoking % all of the "rtbMake..." executive sripts found within the ExampleScenes/ % folder % @@ -19,11 +19,11 @@ function results = rtbTestAllExampleScenes(varargin) % have a name that that includes the name of this m-file, plus the date and % time. % -% rtbTestAllExampleScenes( ... 'outputRoot', outputRoot) specifies the +% rtbRunEpicTest( ... 'outputRoot', outputRoot) specifies the % working folder where to put rendering outputs. The default is from % rtbDefaultHints(). % -% rtbTestAllExampleScenes( ... 'makeFunctions', makeFunctions) specifies a +% rtbRunEpicTest( ... 'makeFunctions', makeFunctions) specifies a % cell array of executive functions to run. The default is to search the % ExampleScenes/ folder for m-files that begin with "rtbMake". % @@ -107,6 +107,7 @@ for ii = find(~isExampleSuccess) disp('----') fprintf('%d %s\n', ii, results(ii).makeFile); disp(results(ii).error) + disp(results(ii).error.message) disp(' ') end diff --git a/Test/Interactive/rtbTestInstallation.m b/Test/Interactive/rtbTestInstallation.m index 743efe03c5e76da43497957f12def047b3e82b34..ada27c2e7035d2605d8dd2a6ea5cb2ae05658f4a 100644 --- a/Test/Interactive/rtbTestInstallation.m +++ b/Test/Interactive/rtbTestInstallation.m @@ -34,6 +34,7 @@ doAll = parser.Results.doAll; renderResults = []; comparison = []; + %% Check working folder for write permission. workingFolder = rtbWorkingFolder(); fprintf('Checking working folder:\n'); @@ -64,31 +65,19 @@ fclose(fid); delete(testFile); fprintf(' OK.\n'); -%% Check for Docker, the preferred way to render. -fprintf('Checking for Docker...\n'); -[status, result] = system('docker ps'); -if 0 == status - fprintf(' OK.\n'); -else - fprintf(' Could not invoke Docker: %s.\n', result); -end -%% Check for local install of Renderers. -mitsuba = getpref('Mitsuba'); -if ismac() - checkExists(mitsuba.app, 'Checking for Mitsuba App...'); -else - checkExists(mitsuba.executable, 'Checking for Mitsuba Executable...'); +%% Check for native system libs and executables. +[status, ~, advice] = rtbCheckNativeDependencies(); +if 0 ~= status + error(advice); end -pbrt = getpref('PBRT'); -checkExists(pbrt.executable, 'Checking for PBRT Executable...'); %% Render some example scenes. if doAll fprintf('\nTesting rendering with all example scripts.\n'); fprintf('This might take a while.\n'); - renderResults = rtbTestAllExampleScenes([], []); + renderResults = rtbRunEpicTest([], []); else testScenes = { ... @@ -99,7 +88,7 @@ else fprintf('\nTesting rendering with %d example scripts.\n', numel(testScenes)); fprintf('You should see several figures with rendered images.\n\n'); - renderResults = rtbTestAllExampleScenes('makeFunctions', testScenes); + renderResults = rtbRunEpicTest('makeFunctions', testScenes); end @@ -107,26 +96,15 @@ if all([renderResults.isSuccess]) fprintf('\nYour RenderToolbox4 installation seems to be working!\n'); end + %% Compare renderings to reference renderings? if ~isempty(referenceRoot) localRoot = rtbWorkingFolder(); fprintf('\nComparing local renderings\n %s\n', localRoot); fprintf('with reference renderings\n %s\n', referenceRoot); fprintf('You should see several more figures.\n\n'); - comparison = rtbCompareAllExampleScenes(localRoot, referenceRoot, '', 2); + comparison = rtbRunEpicComparison(localRoot, referenceRoot, '', 2); else fprintf('\nNo referenceRoot provided. Local renderings\n'); fprintf('will not be compared with reference renderings.\n'); end - - -%% Check whether something exists and print messages. -function exists = checkExists(filePath, message) -fprintf('%s\n', message); -exists = 0 ~= exist(filePath, 'dir') || 0 ~= exist(filePath, 'file'); -if exists - fprintf(' Found %s\n', filePath); - fprintf(' OK.\n'); -else - fprintf(' Could not find %s\n', filePath); -end diff --git a/rdt-config-render-toolbox.json b/rdt-config-render-toolbox.json new file mode 100644 index 0000000000000000000000000000000000000000..5f415f000be90c33f13372f2e019b5b8704ce5cc --- /dev/null +++ b/rdt-config-render-toolbox.json @@ -0,0 +1,8 @@ +{ + "serverUrl": "http://52.32.77.154/", + "repositoryUrl": "http://52.32.77.154/repository/RenderToolbox/", + "username": "guest", + "password": "", + "repositoryName": "RenderToolbox" +} + diff --git a/rtbCheckNativeDependencies.m b/rtbCheckNativeDependencies.m new file mode 100644 index 0000000000000000000000000000000000000000..88b7093dd2017e8091e8826460a661870f1a6644 --- /dev/null +++ b/rtbCheckNativeDependencies.m @@ -0,0 +1,100 @@ +function [status, result, advice] = rtbCheckNativeDependencies() +% Check whether required native dependencies are installed. +% +% [status, result, advice] = rtbCheckNativeDependencies() checks the local +% system for native dependencies, like renderers and shared libraries. +% Returns a status code which is non-zero if some dependency was missing. +% Also returns a result, such as an error code about the missing +% dependency. Finally, returns a string with advice about how to obtain +% a missing dependency, if any. +% +% [status, result, advice] = rtbCheckNativeDependencies() +% +%%% RenderToolbox4 Copyright (c) 2012-2017 The RenderToolbox Team. +%%% About Us://github.com/RenderToolbox/RenderToolbox4/wiki/About-Us +%%% RenderToolbox4 is released under the MIT License. See LICENSE file. + + +%% Check for OpenEXR library, also known as IlmImf. +if ismac() + % assume homebrew + findLibCommand = 'brew list | grep openexr'; +else + findLibCommand = 'ldconfig -p | grep libIlmImf'; +end +openExr = checkSystem('OpenEXR', ... + findLibCommand, ... + 'It looks like the OpenEXR library is not installed. Please visit http://www.openexr.com/. You might also try "sudo apt-get install openexr" or similar.'); + + +%% OpenEXR is required. +if 0 ~= openExr.status + status = openExr.status; + result = openExr.result; + advice = openExr.advice; + return; +end + + +%% Check for Docker, the preferred way to obtain renderers. +docker = checkSystem('Docker', ... + 'docker ps', ... + 'It looks like Docker is not installed. Please visit https://github.com/RenderToolbox/RenderToolbox4/wiki/Docker.'); + + +%% Docker can cover both renderers. +if 0 == docker.status + status = 0; + result = 'Local dependencies were found.'; + advice = ''; + return; +end + + +%% Check for a local installation of the Mitsuba renderer. +mitsuba = checkSystem('Mitsuba', ... + 'which mitsuba', ... + 'It looks like Mitsuba is not installed. Please visit https://www.mitsuba-renderer.org/. Or, consider installing Docker so that RenderToolbox can get Mitsuba for you.'); + + +%% Check for a local installation of the PBRT renderer. +pbrt = checkSystem('PBRT', ... + 'which pbrt', ... + 'It looks like PBRT is not installed. Please visit https://github.com/ydnality/pbrt-v2-spectral. Or, consider installing Docker so that RenderToolbox can get PBRT for you.'); + + +%% Check for both renderers. +if 0 ~= mitsuba.status + status = mitsuba.status; + result = mitsuba.result; + advice = mitsuba.advice; + return; +end + +if 0 ~= pbrt.status + status = pbrt.status; + result = pbrt.result; + advice = pbrt.advice; + return; +end + + +%% Looks good from here. +status = 0; +result = 'Local dependencies were found.'; +advice = ''; + + +%% Check whether something exists and print messages. +function info = checkSystem(name, command, advice) +fprintf('Checking for %s:\n', name); +fprintf(' %s\n', command); +info.name = name; +info.advice = advice; +[info.status, result] = system(command); +info.result = strtrim(result); +if 0 == info.status + fprintf(' OK.\n'); +else + fprintf(' Not found. Status %d, result <%s>.\n', info.status, info.result); +end