//------------------------------------------------------------------------------ // // Copyright (c) Microsoft Corporation. All rights reserved. // //------------------------------------------------------------------------------ // System includes //#include "stdafx.h" #include #define _USE_MATH_DEFINES #include #include #include #include #include #include #pragma warning(push) #pragma warning(disable:6255) #pragma warning(disable:6263) #pragma warning(disable:4995) #include "ppl.h" #pragma warning(pop) // Project includes #include "KinectFusionHelper.h" /// /// Set Identity in a Matrix4 /// /// The matrix to set to identity void SetIdentityMatrix(Matrix4 &mat) { mat.M11 = 1; mat.M12 = 0; mat.M13 = 0; mat.M14 = 0; mat.M21 = 0; mat.M22 = 1; mat.M23 = 0; mat.M24 = 0; mat.M31 = 0; mat.M32 = 0; mat.M33 = 1; mat.M34 = 0; mat.M41 = 0; mat.M42 = 0; mat.M43 = 0; mat.M44 = 1; } /// /// Extract translation Vector3 from the Matrix4 4x4 transformation in M41,M42,M43 /// /// The transform matrix. /// Array of 3 floating point values for translation. void ExtractVector3Translation(const Matrix4 &transform, _Out_cap_c_(3) float *translation) { translation[0] = transform.M41; translation[1] = transform.M42; translation[2] = transform.M43; } /// /// Extract translation Vector3 from the 4x4 Matrix in M41,M42,M43 /// /// The transform matrix. /// Returns a Vector3 containing the translation. Vector3 ExtractVector3Translation(const Matrix4 &transform) { Vector3 translation; translation.x = transform.M41; translation.y = transform.M42; translation.z = transform.M43; return translation; } /// /// Extract 3x3 rotation from the 4x4 Matrix and return in new Matrix4 /// /// The transform matrix. /// Returns a Matrix4 containing the rotation. Matrix4 Extract3x3Rotation(const Matrix4 &transform) { Matrix4 rotation; rotation.M11 = transform.M11; rotation.M12 = transform.M12; rotation.M13 = transform.M13; rotation.M14 = 0; rotation.M21 = transform.M21; rotation.M22 = transform.M22; rotation.M23 = transform.M23; rotation.M24 = 0; rotation.M31 = transform.M31; rotation.M32 = transform.M32; rotation.M33 = transform.M33; rotation.M34 = 0; rotation.M41 = 0; rotation.M42 = 0; rotation.M43 = 0; rotation.M44 = 1; return rotation; } /// /// Extract 3x3 rotation matrix from the Matrix4 4x4 transformation: /// Then convert to Euler angles. /// /// The transform matrix. /// Array of 3 floating point values for euler angles. void ExtractRot2Euler(const Matrix4 &transform, _Out_cap_c_(3) float *rotation) { float phi = atan2f(transform.M23, transform.M33); float theta = asinf(-transform.M13); float psi = atan2f(transform.M12, transform.M11); rotation[0] = phi; // This is rotation about x,y,z, or pitch, yaw, roll respectively rotation[1] = theta; rotation[2] = psi; } /// /// Test whether the camera moved too far between sequential frames by looking at starting and end transformation matrix. /// We assume that if the camera moves or rotates beyond a reasonable threshold, that we have lost track. /// Note that on lower end machines, if the processing frame rate decreases below 30Hz, this limit will potentially have /// to be increased as frames will be dropped and hence there will be a greater motion between successive frames. /// /// The transform matrix from the previous frame. /// The transform matrix from the current frame. /// The maximum translation in meters we expect per x,y,z component between frames under normal motion. /// The maximum rotation in degrees we expect about the x,y,z axes between frames under normal motion. /// true if camera transformation is greater than the threshold, otherwise false bool CameraTransformFailed(const Matrix4 &T_initial, const Matrix4 &T_final, float maxTrans, float maxRotDegrees) { // Check if the transform is too far out to be reasonable float deltaTrans = maxTrans; float angDeg = maxRotDegrees; float deltaRot = (angDeg * (float)M_PI) / 180.0f; // Calculate the deltas float eulerInitial[3]; float eulerFinal[3]; ExtractRot2Euler(T_initial, eulerInitial); ExtractRot2Euler(T_final, eulerFinal); float transInitial[3]; float transFinal[3]; ExtractVector3Translation(T_initial, transInitial); ExtractVector3Translation(T_final, transFinal); bool failRot = false; bool failTrans = false; float rDeltas[3]; float tDeltas[3]; static const float pi = static_cast(M_PI); for (int i = 0; i < 3; i++) { // Handle when one angle is near PI, and the other is near -PI. if (eulerInitial[i] >= (pi - deltaRot) && eulerFinal[i] < (deltaRot - pi)) { eulerInitial[i] -= pi * 2; } else if (eulerFinal[i] >= (pi - deltaRot) && eulerInitial[i] < (deltaRot - pi)) { eulerFinal[i] -= pi * 2; } rDeltas[i] = eulerInitial[i] - eulerFinal[i]; tDeltas[i] = transInitial[i] - transFinal[i]; if (fabs(rDeltas[i]) > deltaRot) { failRot = true; break; } if (fabs(tDeltas[i]) > deltaTrans) { failTrans = true; break; } } return failRot || failTrans; } /// /// Invert/Transpose the 3x3 Rotation Matrix Component of a 4x4 matrix /// /// The rotation matrix to invert. void InvertRotation(Matrix4 &rot) { // Invert equivalent to a transpose for 3x3 rotation rotrices when orthogonal float tmp = rot.M12; rot.M12 = rot.M21; rot.M21 = tmp; tmp = rot.M13; rot.M13 = rot.M31; rot.M31 = tmp; tmp = rot.M23; rot.M23 = rot.M32; rot.M32 = tmp; } /// /// Negate the 3x3 Rotation Matrix Component of a 4x4 matrix /// /// The rotation matrix to negate. void NegateRotation(Matrix4 &rot) { rot.M11 = -rot.M11; rot.M12 = -rot.M12; rot.M13 = -rot.M13; rot.M21 = -rot.M21; rot.M22 = -rot.M22; rot.M23 = -rot.M23; rot.M31 = -rot.M31; rot.M32 = -rot.M32; rot.M33 = -rot.M33; } /// /// Rotate a vector with the 3x3 Rotation Matrix Component of a 4x4 matrix /// /// The Vector3 to rotate. /// Rotation matrix. Vector3 RotateVector(const Vector3 &vec, const Matrix4 & rot) { // we only use the rotation component here Vector3 result; result.x = (rot.M11 * vec.x) + (rot.M21 * vec.y) + (rot.M31 * vec.z); result.y = (rot.M12 * vec.x) + (rot.M22 * vec.y) + (rot.M32 * vec.z); result.z = (rot.M13 * vec.x) + (rot.M23 * vec.y) + (rot.M33 * vec.z); return result; } /// /// Invert Matrix4 Pose either from WorldToCameraTransform (view) matrix to CameraToWorldTransform camera pose matrix (world/SE3) or vice versa /// /// The camera pose transform matrix. /// Returns a Matrix4 containing the inverted camera pose. Matrix4 InvertMatrix4Pose(const Matrix4 &transform) { // Given the SE3 world transform transform T = [R|t], the inverse view transform matrix is simply: // T^-1 = [R^T | -R^T . t ] // This also works the opposite way to get the world transform, given the view transform matrix. Matrix4 rotation = Extract3x3Rotation(transform); Matrix4 invRotation = rotation; InvertRotation(invRotation); // invert(transpose) 3x3 rotation Matrix4 negRotation = invRotation; NegateRotation(negRotation); // negate 3x3 rotation Vector3 translation = ExtractVector3Translation(transform); Vector3 invTranslation = RotateVector(translation, negRotation); // Add the translation back in invRotation.M41 = invTranslation.x; invRotation.M42 = invTranslation.y; invRotation.M43 = invTranslation.z; return invRotation; } /// /// Write Binary .STL file /// see http://en.wikipedia.org/wiki/STL_(file_format) for STL format /// /// The Kinect Fusion mesh object. /// The full path and filename of the file to save. /// Flag to determine whether the Y and Z values are flipped on save. /// indicates success or failure HRESULT WriteBinarySTLMeshFile(INuiFusionColorMesh *mesh, LPOLESTR lpOleFileName, bool flipYZ) { HRESULT hr = S_OK; if (NULL == mesh) { return E_INVALIDARG; } unsigned int numVertices = mesh->VertexCount(); unsigned int numTriangleIndices = mesh->TriangleVertexIndexCount(); unsigned int numTriangles = numVertices / 3; if (0 == numVertices || 0 == numTriangleIndices || 0 != numVertices % 3 || numVertices != numTriangleIndices) { return E_INVALIDARG; } const Vector3 *vertices = NULL; hr = mesh->GetVertices(&vertices); if (FAILED(hr)) { return hr; } const Vector3 *normals = NULL; hr = mesh->GetNormals(&normals); if (FAILED(hr)) { return hr; } const int *triangleIndices = NULL; hr = mesh->GetTriangleIndices(&triangleIndices); if (FAILED(hr)) { return hr; } // Open File std::string filename = std::wstring_convert>().to_bytes(lpOleFileName); FILE *meshFile = NULL; errno_t err = fopen_s(&meshFile, filename.c_str(), "wb"); // Could not open file for writing - return if (0 != err || NULL == meshFile) { return E_ACCESSDENIED; } // Write the header line const unsigned char header[80] = {0}; // initialize all values to 0 fwrite(&header, sizeof(unsigned char), ARRAYSIZE(header), meshFile); // Write number of triangles fwrite(&numTriangles, sizeof(int), 1, meshFile); // Sequentially write the normal, 3 vertices of the triangle and attribute, for each triangle for (unsigned int t=0; t < numTriangles; ++t) { Vector3 normal = normals[t*3]; if (flipYZ) { normal.y = -normal.y; normal.z = -normal.z; } // Write normal fwrite(&normal, sizeof(float), 3, meshFile); // Write vertices for (unsigned int v=0; v<3; v++) { Vector3 vertex = vertices[(t*3) + v]; if (flipYZ) { vertex.y = -vertex.y; vertex.z = -vertex.z; } fwrite(&vertex, sizeof(float), 3, meshFile); } unsigned short attribute = 0; fwrite(&attribute, sizeof(unsigned short), 1, meshFile); } fflush(meshFile); fclose(meshFile); return hr; } /// /// Write ASCII Wavefront .OBJ file /// See http://en.wikipedia.org/wiki/Wavefront_.obj_file for .OBJ format /// /// The Kinect Fusion mesh object. /// The full path and filename of the file to save. /// Flag to determine whether the Y and Z values are flipped on save. /// indicates success or failure HRESULT WriteAsciiObjMeshFile(INuiFusionColorMesh *mesh, LPOLESTR lpOleFileName, bool flipYZ) { HRESULT hr = S_OK; if (NULL == mesh) { return E_INVALIDARG; } unsigned int numVertices = mesh->VertexCount(); unsigned int numTriangleIndices = mesh->TriangleVertexIndexCount(); unsigned int numTriangles = numVertices / 3; if (0 == numVertices || 0 == numTriangleIndices || 0 != numVertices % 3 || numVertices != numTriangleIndices) { return E_INVALIDARG; } const Vector3 *vertices = NULL; hr = mesh->GetVertices(&vertices); if (FAILED(hr)) { return hr; } const Vector3 *normals = NULL; hr = mesh->GetNormals(&normals); if (FAILED(hr)) { return hr; } const int *triangleIndices = NULL; hr = mesh->GetTriangleIndices(&triangleIndices); if (FAILED(hr)) { return hr; } // Open File std::string filename = std::wstring_convert>().to_bytes(lpOleFileName); FILE *meshFile = NULL; errno_t err = fopen_s(&meshFile, filename.c_str(), "wt"); // Could not open file for writing - return if (0 != err || NULL == meshFile) { return E_ACCESSDENIED; } // Write the header line std::string header = "#\n# OBJ file created by Microsoft Kinect Fusion\n#\n"; fwrite(header.c_str(), sizeof(char), header.length(), meshFile); const unsigned int bufSize = MAX_PATH*3; char outStr[bufSize]; int written = 0; if (flipYZ) { // Sequentially write the 3 vertices of the triangle, for each triangle for (unsigned int t=0, vertexIndex=0; t < numTriangles; ++t, vertexIndex += 3) { written = sprintf_s(outStr, bufSize, "v %f %f %f\nv %f %f %f\nv %f %f %f\n", vertices[vertexIndex].x, -vertices[vertexIndex].y, -vertices[vertexIndex].z, vertices[vertexIndex+1].x, -vertices[vertexIndex+1].y, -vertices[vertexIndex+1].z, vertices[vertexIndex+2].x, -vertices[vertexIndex+2].y, -vertices[vertexIndex+2].z); fwrite(outStr, sizeof(char), written, meshFile); } // Sequentially write the 3 normals of the triangle, for each triangle for (unsigned int t=0, normalIndex=0; t < numTriangles; ++t, normalIndex += 3) { written = sprintf_s(outStr, bufSize, "n %f %f %f\nn %f %f %f\nn %f %f %f\n", normals[normalIndex].x, -normals[normalIndex].y, -normals[normalIndex].z, normals[normalIndex+1].x, -normals[normalIndex+1].y, -normals[normalIndex+1].z, normals[normalIndex+2].x, -normals[normalIndex+2].y, -normals[normalIndex+2].z); fwrite(outStr, sizeof(char), written, meshFile); } } else { // Sequentially write the 3 vertices of the triangle, for each triangle for (unsigned int t=0, vertexIndex=0; t < numTriangles; ++t, vertexIndex += 3) { written = sprintf_s(outStr, bufSize, "v %f %f %f\nv %f %f %f\nv %f %f %f\n", vertices[vertexIndex].x, vertices[vertexIndex].y, vertices[vertexIndex].z, vertices[vertexIndex+1].x, vertices[vertexIndex+1].y, vertices[vertexIndex+1].z, vertices[vertexIndex+2].x, vertices[vertexIndex+2].y, vertices[vertexIndex+2].z); fwrite(outStr, sizeof(char), written, meshFile); } // Sequentially write the 3 normals of the triangle, for each triangle for (unsigned int t=0, normalIndex=0; t < numTriangles; ++t, normalIndex += 3) { written = sprintf_s(outStr, bufSize, "n %f %f %f\nn %f %f %f\nn %f %f %f\n", normals[normalIndex].x, normals[normalIndex].y, normals[normalIndex].z, normals[normalIndex+1].x, normals[normalIndex+1].y, normals[normalIndex+1].z, normals[normalIndex+2].x, normals[normalIndex+2].y, normals[normalIndex+2].z); fwrite(outStr, sizeof(char), written, meshFile); } } // Sequentially write the 3 vertex indices of the triangle face, for each triangle // Note this is typically 1-indexed in an OBJ file when using absolute referencing! for (unsigned int t=0, baseIndex=1; t < numTriangles; ++t, baseIndex += 3) // Start at baseIndex=1 for the 1-based indexing. { written = sprintf_s(outStr, bufSize, "f %u//%u %u//%u %u//%u\n", baseIndex, baseIndex, baseIndex+1, baseIndex+1, baseIndex+2, baseIndex+2); fwrite(outStr, sizeof(char), written, meshFile); } // Note: we do not have texcoords to store, if we did, we would put the index of the texcoords between the vertex and normal indices (i.e. between the two slashes //) in the string above fflush(meshFile); fclose(meshFile); return hr; } /// /// Write ASCII .PLY file /// See http://paulbourke.net/dataformats/ply/ for .PLY format /// /// The Kinect Fusion mesh object. /// The full path and filename of the file to save. /// Flag to determine whether the Y and Z values are flipped on save. /// Set this true to write out the surface color to the file when it has been captured. /// indicates success or failure HRESULT WriteAsciiPlyMeshFile(INuiFusionColorMesh *mesh, LPOLESTR lpOleFileName, bool flipYZ, bool outputColor) { HRESULT hr = S_OK; if (NULL == mesh) { return E_INVALIDARG; } unsigned int numVertices = mesh->VertexCount(); unsigned int numTriangleIndices = mesh->TriangleVertexIndexCount(); unsigned int numTriangles = numVertices / 3; unsigned int numColors = mesh->ColorCount(); if (0 == numVertices || 0 == numTriangleIndices || 0 != numVertices % 3 || numVertices != numTriangleIndices || (outputColor && numVertices != numColors)) { return E_INVALIDARG; } const Vector3 *vertices = NULL; hr = mesh->GetVertices(&vertices); if (FAILED(hr)) { return hr; } const int *triangleIndices = NULL; hr = mesh->GetTriangleIndices(&triangleIndices); if (FAILED(hr)) { return hr; } const int *colors = NULL; if (outputColor) { hr = mesh->GetColors(&colors); if (FAILED(hr)) { return hr; } } // Open File std::string filename = std::wstring_convert>().to_bytes(lpOleFileName); FILE *meshFile = NULL; errno_t err = fopen_s(&meshFile, filename.c_str(), "wt"); // Could not open file for writing - return if (0 != err || NULL == meshFile) { return E_ACCESSDENIED; } // Write the header line std::string header = "ply\nformat ascii 1.0\ncomment file created by Microsoft Kinect Fusion\n"; fwrite(header.c_str(), sizeof(char), header.length(), meshFile); const unsigned int bufSize = MAX_PATH*3; char outStr[bufSize]; int written = 0; if (outputColor) { // Elements are: x,y,z, r,g,b written = sprintf_s(outStr, bufSize, "element vertex %u\nproperty float x\nproperty float y\nproperty float z\nproperty uchar red\nproperty uchar green\nproperty uchar blue\n", numVertices); fwrite(outStr, sizeof(char), written, meshFile); } else { // Elements are: x,y,z written = sprintf_s(outStr, bufSize, "element vertex %u\nproperty float x\nproperty float y\nproperty float z\n", numVertices); fwrite(outStr, sizeof(char), written, meshFile); } written = sprintf_s(outStr, bufSize, "element face %u\nproperty list uchar int vertex_index\nend_header\n", numTriangles); fwrite(outStr, sizeof(char), written, meshFile); if (flipYZ) { if (outputColor) { // Sequentially write the 3 vertices of the triangle, for each triangle for (unsigned int t=0, vertexIndex=0; t < numTriangles; ++t, vertexIndex += 3) { unsigned int color0 = colors[vertexIndex]; unsigned int color1 = colors[vertexIndex+1]; unsigned int color2 = colors[vertexIndex+2]; written = sprintf_s(outStr, bufSize, "%f %f %f %u %u %u\n%f %f %f %u %u %u\n%f %f %f %u %u %u\n", vertices[vertexIndex].x, -vertices[vertexIndex].y, -vertices[vertexIndex].z, ((color0 >> 16) & 255), ((color0 >> 8) & 255), (color0 & 255), vertices[vertexIndex+1].x, -vertices[vertexIndex+1].y, -vertices[vertexIndex+1].z, ((color1 >> 16) & 255), ((color1 >> 8) & 255), (color1 & 255), vertices[vertexIndex+2].x, -vertices[vertexIndex+2].y, -vertices[vertexIndex+2].z, ((color2 >> 16) & 255), ((color2 >> 8) & 255), (color2 & 255)); fwrite(outStr, sizeof(char), written, meshFile); } } else { // Sequentially write the 3 vertices of the triangle, for each triangle for (unsigned int t=0, vertexIndex=0; t < numTriangles; ++t, vertexIndex += 3) { written = sprintf_s(outStr, bufSize, "%f %f %f\n%f %f %f\n%f %f %f\n", vertices[vertexIndex].x, -vertices[vertexIndex].y, -vertices[vertexIndex].z, vertices[vertexIndex+1].x, -vertices[vertexIndex+1].y, -vertices[vertexIndex+1].z, vertices[vertexIndex+2].x, -vertices[vertexIndex+2].y, -vertices[vertexIndex+2].z); fwrite(outStr, sizeof(char), written, meshFile); } } } else { if (outputColor) { // Sequentially write the 3 vertices of the triangle, for each triangle for (unsigned int t=0, vertexIndex=0; t < numTriangles; ++t, vertexIndex += 3) { unsigned int color0 = colors[vertexIndex]; unsigned int color1 = colors[vertexIndex+1]; unsigned int color2 = colors[vertexIndex+2]; written = sprintf_s(outStr, bufSize, "%f %f %f %u %u %u\n%f %f %f %u %u %u\n%f %f %f %u %u %u\n", vertices[vertexIndex].x, vertices[vertexIndex].y, vertices[vertexIndex].z, ((color0 >> 16) & 255), ((color0 >> 8) & 255), (color0 & 255), vertices[vertexIndex+1].x, vertices[vertexIndex+1].y, vertices[vertexIndex+1].z, ((color1 >> 16) & 255), ((color1 >> 8) & 255), (color1 & 255), vertices[vertexIndex+2].x, vertices[vertexIndex+2].y, vertices[vertexIndex+2].z, ((color2 >> 16) & 255), ((color2 >> 8) & 255), (color2 & 255)); fwrite(outStr, sizeof(char), written, meshFile); } } else { // Sequentially write the 3 vertices of the triangle, for each triangle for (unsigned int t=0, vertexIndex=0; t < numTriangles; ++t, vertexIndex += 3) { written = sprintf_s(outStr, bufSize, "%f %f %f\n%f %f %f\n%f %f %f\n", vertices[vertexIndex].x, vertices[vertexIndex].y, vertices[vertexIndex].z, vertices[vertexIndex+1].x, vertices[vertexIndex+1].y, vertices[vertexIndex+1].z, vertices[vertexIndex+2].x, vertices[vertexIndex+2].y, vertices[vertexIndex+2].z); fwrite(outStr, sizeof(char), written, meshFile); } } } // Sequentially write the 3 vertex indices of the triangle face, for each triangle (0-referenced in PLY) for (unsigned int t=0, baseIndex=0; t < numTriangles; ++t, baseIndex += 3) { written = sprintf_s(outStr, bufSize, "3 %u %u %u\n", baseIndex, baseIndex+1, baseIndex+2); fwrite(outStr, sizeof(char), written, meshFile); } fflush(meshFile); fclose(meshFile); return hr; } /// /// Write ASCII Wavefront .OBJ file with bitmap texture and material file /// See http://en.wikipedia.org/wiki/Wavefront_.obj_file for .OBJ format /// /// The Kinect Fusion mesh object. /// The full path and filename of the file to save. /// Flag to determine whether the Y and Z values are flipped on save. /// The Kinect Fusion color texture image. /// Three Vector3 texture coordinates per mesh triangle, normalized by the image size. /// S_OK on success, otherwise failure code HRESULT WriteTexturedeAsciiObjMeshFile(INuiFusionColorMesh *mesh, LPOLESTR lpOleFileName, bool flipYZ, NUI_FUSION_IMAGE_FRAME *pTexture, const std::vector &texcoords) { HRESULT hr = S_OK; if (nullptr == mesh || nullptr == pTexture) { return E_INVALIDARG; } unsigned int numVertices = mesh->VertexCount(); unsigned int numTriangleIndices = mesh->TriangleVertexIndexCount(); unsigned int numTriangles = numVertices / 3; if (0 == numVertices || 0 == numTriangleIndices || 0 != numVertices % 3 || numVertices != numTriangleIndices) { return E_INVALIDARG; } const Vector3 *vertices = NULL; hr = mesh->GetVertices(&vertices); if (FAILED(hr)) { return hr; } const Vector3 *normals = NULL; hr = mesh->GetNormals(&normals); if (FAILED(hr)) { return hr; } const int *triangleIndices = NULL; hr = mesh->GetTriangleIndices(&triangleIndices); if (FAILED(hr)) { return hr; } // Open File std::string filename = std::wstring_convert>().to_bytes(lpOleFileName); FILE *meshFile = NULL; errno_t err = fopen_s(&meshFile, filename.c_str(), "wt"); // Could not open file for writing - return if (0 != err || NULL == meshFile) { return E_ACCESSDENIED; } // Split the name and extension std::string mtlfilename = filename + ".mtl"; // Open the material file FILE *mtlFile = NULL; err = fopen_s(&mtlFile, mtlfilename.c_str(), "wt"); // Could not open file for writing - return if (0 != err || NULL == mtlFile) { if (meshFile) { fclose(meshFile); } return E_ACCESSDENIED; } // Write the header line std::string header = "#\n# OBJ file created by Microsoft Kinect Fusion\n#\n"; fwrite(header.c_str(), sizeof(char), header.length(), meshFile); // Split to extract path std::string::size_type pos = filename.find_last_of("\\", filename.length()); std::string filenamePath = filename.substr(0,pos+1); std::string filenameRelative = filename.substr(pos+1); // Write that we have an accompanying material file std::string mtlfile = "mtllib " + filenameRelative + ".mtl\n"; fwrite(mtlfile.c_str(), sizeof(char), mtlfile.length(), meshFile); const unsigned int bufSize = MAX_PATH*3; char outStr[bufSize]; int written = 0; if (flipYZ) { // Sequentially write the 3 vertices of the triangle, for each triangle for (unsigned int t=0, vertexIndex=0; t < numTriangles; ++t, vertexIndex += 3) { written = sprintf_s(outStr, bufSize, "v %f %f %f\nv %f %f %f\nv %f %f %f\n", vertices[vertexIndex].x, -vertices[vertexIndex].y, -vertices[vertexIndex].z, vertices[vertexIndex+1].x, -vertices[vertexIndex+1].y, -vertices[vertexIndex+1].z, vertices[vertexIndex+2].x, -vertices[vertexIndex+2].y, -vertices[vertexIndex+2].z); fwrite(outStr, sizeof(char), written, meshFile); } // Sequentially write the 3 normals of the triangle, for each triangle for (unsigned int t=0, normalIndex=0; t < numTriangles; ++t, normalIndex += 3) { written = sprintf_s(outStr, bufSize, "n %f %f %f\nn %f %f %f\nn %f %f %f\n", normals[normalIndex].x, -normals[normalIndex].y, -normals[normalIndex].z, normals[normalIndex+1].x, -normals[normalIndex+1].y, -normals[normalIndex+1].z, normals[normalIndex+2].x, -normals[normalIndex+2].y, -normals[normalIndex+2].z); fwrite(outStr, sizeof(char), written, meshFile); } } else { // Sequentially write the 3 vertices of the triangle, for each triangle for (unsigned int t=0, vertexIndex=0; t < numTriangles; ++t, vertexIndex += 3) { written = sprintf_s(outStr, bufSize, "v %f %f %f\nv %f %f %f\nv %f %f %f\n", vertices[vertexIndex].x, vertices[vertexIndex].y, vertices[vertexIndex].z, vertices[vertexIndex+1].x, vertices[vertexIndex+1].y, vertices[vertexIndex+1].z, vertices[vertexIndex+2].x, vertices[vertexIndex+2].y, vertices[vertexIndex+2].z); fwrite(outStr, sizeof(char), written, meshFile); } // Sequentially write the 3 normals of the triangle, for each triangle for (unsigned int t=0, normalIndex=0; t < numTriangles; ++t, normalIndex += 3) { written = sprintf_s(outStr, bufSize, "n %f %f %f\nn %f %f %f\nn %f %f %f\n", normals[normalIndex].x, normals[normalIndex].y, normals[normalIndex].z, normals[normalIndex+1].x, normals[normalIndex+1].y, normals[normalIndex+1].z, normals[normalIndex+2].x, normals[normalIndex+2].y, normals[normalIndex+2].z); fwrite(outStr, sizeof(char), written, meshFile); } } // Sequentially write the 3 texture coordinates of the triangle, for each triangle for (unsigned int t=0, texcoordIndex=0; t < numTriangles; ++t, texcoordIndex += 3) { written = sprintf_s(outStr, bufSize, "vt %f %f\nvt %f %f\nvt %f %f\n", texcoords[texcoordIndex].x, texcoords[texcoordIndex].y, texcoords[texcoordIndex+1].x, texcoords[texcoordIndex+1].y, texcoords[texcoordIndex+2].x, texcoords[texcoordIndex+2].y); fwrite(outStr, sizeof(char), written, meshFile); } // Write that we are using material0 (i.e. the texture) std::string material = "usemtl material0\n"; fwrite(material.c_str(), sizeof(char), material.length(), meshFile); // Sequentially write the 3 vertex indices of the triangle face, for each triangle // Note this is typically 1-indexed in an OBJ file when using absolute referencing! for (unsigned int t=0, baseIndex=1; t < numTriangles; ++t, baseIndex += 3) // Start at baseIndex=1 for the 1-based indexing. { written = sprintf_s(outStr, bufSize, "f %u/%u/%u %u/%u/%u %u/%u/%u\n", baseIndex, baseIndex, baseIndex, baseIndex+1, baseIndex+1, baseIndex+1, baseIndex+2, baseIndex+2, baseIndex+2); fwrite(outStr, sizeof(char), written, meshFile); } fflush(meshFile); fclose(meshFile); // Write the material description file header = "#\n# OBJ file created by Microsoft Kinect Fusion\n#\n"; fwrite(header.c_str(), sizeof(char), header.length(), mtlFile); material = "newmtl material0\n"; fwrite(material.c_str(), sizeof(char), material.length(), mtlFile); // Create the texture filename std::string textureFilename = filenameRelative + ".bmp"; // Write the generic materials definition together with the texture filename. std::string mtldescription ="Ka 1.000000 1.000000 1.000000\n" "Kd 1.000000 1.000000 1.000000\n" "Ks 0.000000 0.000000 0.000000\n" "Tr 1.000000\n" "illum 1\n" "Ns 0.000000\n" "map_Kd " + textureFilename + "\n"; fwrite(mtldescription.c_str(), sizeof(char), mtldescription.length(), mtlFile); fflush(mtlFile); fclose(mtlFile); std::string textureFilenamePath = filenamePath + textureFilename; CA2W pszW( textureFilenamePath.c_str() ); // Write out the texture NUI_FUSION_BUFFER *fusionColorBuffer = pTexture->pFrameBuffer; if (fusionColorBuffer->Pitch == 0) { return E_FAIL; } // Save the texture hr = SaveBMPFile(pszW, fusionColorBuffer->pBits, pTexture->width, pTexture->height); return hr; } /// /// Returns whether this is running as a 32 or 64bit application. /// /// TRUE indicates a 64bit app. BOOL Is64BitApp() { #if defined(_WIN64) // If _WIN64 is defined, we are a 64-bit version as // this will only be defined on Win64 return TRUE; #else // 32-bit programs run on both 32-bit and 64-bit Windows with WOW64, // however the restrictions are the same for our application. return FALSE; #endif } /// /// Write 32bit BMP image file /// /// The full path and filename of the file to save. /// A pointer to the image bytes to save. /// The width of the image to save. /// The width of the image to save. /// indicates success or failure HRESULT SaveBMPFile(LPCWSTR pszFile, const byte *pImageBytes, unsigned int width, unsigned int height) { // Each pixel is 8 bits per color, 4 values interleaved (R,G,B,A) = 32 bits total const WORD cBitsPerColor = 8; const WORD cColorValues = 4; WORD cColorBits = cBitsPerColor * cColorValues; // No need to pad the array as we already have 32bit pixels. DWORD imageByteSize = static_cast(width) * static_cast(height) * static_cast(cColorBits/8); // However, we may need to swap R and B byte ordering. // Set headers BITMAPFILEHEADER bmfh; BITMAPINFOHEADER info; memset (&bmfh, 0, sizeof(BITMAPFILEHEADER)); memset (&info, 0, sizeof(BITMAPINFOHEADER)); bmfh.bfType = 0x4d42; // Magic number "BM" bmfh.bfReserved1 = 0; bmfh.bfReserved2 = 0; bmfh.bfSize = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + imageByteSize; bmfh.bfOffBits = 0x36; info.biSize = sizeof(BITMAPINFOHEADER); info.biWidth = width; info.biHeight = height; info.biPlanes = 1; info.biBitCount = cColorBits; info.biCompression = BI_RGB; info.biSizeImage = 0; info.biXPelsPerMeter = 0x0ec4; info.biYPelsPerMeter = 0x0ec4; info.biClrUsed = 0; info.biClrImportant = 0; // Open file and write HANDLE file = CreateFileW(pszFile, GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if (INVALID_HANDLE_VALUE == file) { return E_INVALIDARG; } unsigned long bytesWritten = 0; if (FALSE == WriteFile(file, &bmfh, sizeof(BITMAPFILEHEADER), &bytesWritten, NULL)) { CloseHandle(file); return E_FAIL; } if (FALSE == WriteFile(file, &info, sizeof(BITMAPINFOHEADER), &bytesWritten, NULL)) { CloseHandle(file); return E_FAIL; } if (FALSE == WriteFile(file, pImageBytes, imageByteSize, &bytesWritten, NULL)) { CloseHandle(file); return E_FAIL; } CloseHandle(file); return S_OK; } /// /// Copy an image with identical sizes and parameters. /// /// A pointer to the source image. /// A pointer to the destination image. /// indicates success or failure HRESULT CopyImageFrame(const NUI_FUSION_IMAGE_FRAME *pSrc, const NUI_FUSION_IMAGE_FRAME *pDest) { HRESULT hr = S_OK; if (nullptr == pSrc || nullptr == pSrc->pFrameBuffer || nullptr == pDest || nullptr == pDest->pFrameBuffer) { return E_INVALIDARG; } if (pSrc->imageType != pDest->imageType) { return E_INVALIDARG; } if (0 == pSrc->width || 0 == pSrc->height || pSrc->width != pDest->width || pSrc->height != pDest->height) { return E_NOINTERFACE; } NUI_FUSION_BUFFER *srcImageFrameBuffer = pSrc->pFrameBuffer; // Make sure we've received valid data if (srcImageFrameBuffer->Pitch != 0) { NUI_FUSION_BUFFER *destImageFrameBuffer = pDest->pFrameBuffer; // Make sure we've received valid data if (destImageFrameBuffer->Pitch != 0) { // Copy errno_t err = memcpy_s( destImageFrameBuffer->pBits, destImageFrameBuffer->Pitch * pDest->height, srcImageFrameBuffer->pBits, srcImageFrameBuffer->Pitch * pSrc->height); if (0 != err) { hr = E_FAIL; } } } return hr; } /// /// Color the residual/delta image from the AlignDepthFloatToReconstruction call /// /// A pointer to the source FloatDeltaFromReference image. /// A pointer to the destination color ShadedDeltaFromReference image. /// S_OK on success, otherwise failure code HRESULT ColorResiduals(const NUI_FUSION_IMAGE_FRAME *pFloatDeltaFromReference, const NUI_FUSION_IMAGE_FRAME *pShadedDeltaFromReference) { if (nullptr == pShadedDeltaFromReference || nullptr == pFloatDeltaFromReference) { return E_FAIL; } if (nullptr == pShadedDeltaFromReference->pFrameBuffer || nullptr == pFloatDeltaFromReference->pFrameBuffer) { return E_NOINTERFACE; } if (pFloatDeltaFromReference->imageType != NUI_FUSION_IMAGE_TYPE_FLOAT || pShadedDeltaFromReference->imageType != NUI_FUSION_IMAGE_TYPE_COLOR) { return E_INVALIDARG; } unsigned int width = pShadedDeltaFromReference->width; unsigned int height = pShadedDeltaFromReference->height; if (width != pFloatDeltaFromReference->width || height != pFloatDeltaFromReference->height) { return E_INVALIDARG; } if (pShadedDeltaFromReference->pFrameBuffer->Pitch == 0 || pFloatDeltaFromReference->pFrameBuffer->Pitch == 0) { return E_INVALIDARG; } unsigned int *pColorBuffer = reinterpret_cast(pShadedDeltaFromReference->pFrameBuffer->pBits); const float *pFloatBuffer = reinterpret_cast(pFloatDeltaFromReference->pFrameBuffer->pBits); Concurrency::parallel_for(0u, height, [&](unsigned int y) { unsigned int* pColorRow = reinterpret_cast(reinterpret_cast(pColorBuffer) + (y * pShadedDeltaFromReference->pFrameBuffer->Pitch)); const float* pFloatRow = reinterpret_cast(reinterpret_cast(pFloatBuffer) + (y * pFloatDeltaFromReference->pFrameBuffer->Pitch)); for (unsigned int x = 0; x < width; ++x) { float residue = pFloatRow[x]; unsigned int color = 0; if (residue <= 1.0f) // Pixel byte ordering: ARGB { color |= (255 << 24); // a color |= (static_cast(255.0f * clamp(1.0f + residue, 0.0f, 1.0f)) << 16); // r color |= (static_cast(255.0f * clamp(1.0f - std::abs(residue), 0.0f, 1.0f)) << 8); // g color |= (static_cast(255.0f * clamp(1.0f - residue, 0.0f, 1.0f))); // b } pColorRow[x] = color; } }); return S_OK; } /// /// Calculate statistics on the residual/delta image from the AlignDepthFloatToReconstruction call. /// /// A pointer to the source FloatDeltaFromReference image. /// S_OK on success, otherwise failure code HRESULT CalculateResidualStatistics(const NUI_FUSION_IMAGE_FRAME *pFloatDeltaFromReference, DeltaFromReferenceImageStatistics *stats) { if (nullptr == pFloatDeltaFromReference || nullptr == stats) { return E_INVALIDARG; } if (nullptr == pFloatDeltaFromReference->pFrameBuffer) { return E_NOINTERFACE; } unsigned int width = pFloatDeltaFromReference->width; unsigned int height = pFloatDeltaFromReference->height; if (0 == width || 0 == height || pFloatDeltaFromReference->pFrameBuffer->Pitch == 0) { return E_INVALIDARG; } const float *pFloatBuffer = reinterpret_cast(pFloatDeltaFromReference->pFrameBuffer->pBits); // Measurement stats std::vector zeroPixelsRow; std::vector validPixelsRow; std::vector invalidDepthOutsideVolumePixelsRow; std::vector validPixelDistanceRow; zeroPixelsRow.resize(height, 0); validPixelsRow.resize(height, 0); invalidDepthOutsideVolumePixelsRow.resize(height, 0); validPixelDistanceRow.resize(height, 0); Concurrency::parallel_for(0u, height, [&](unsigned int y) { const float* pFloatRow = reinterpret_cast(reinterpret_cast(pFloatBuffer) + (y * pFloatDeltaFromReference->pFrameBuffer->Pitch)); for (unsigned int x = 0; x < width; ++x) { float residue = pFloatRow[x]; // If the depth was invalid or the depth back-projected outside the volume, the residual is set to 2.0f // However, if the voxel contents are 0 the residual will also return 0 here. if (residue == 0.0f) { ++zeroPixelsRow[y]; } else if (residue == 2.0f) { ++invalidDepthOutsideVolumePixelsRow[y]; } else if (residue <= 1.0f) // Pixel byte ordering: ARGB { ++validPixelsRow[y]; validPixelDistanceRow[y] += residue; } } }); stats->validPixels = stats->zeroPixels = stats->invalidDepthOutsideVolumePixels = 0; stats->totalValidPixelsDistance = 0; stats->totalPixels = width * height; for (unsigned int y=0; yzeroPixels += zeroPixelsRow[y]; stats->validPixels += validPixelsRow[y]; stats->invalidDepthOutsideVolumePixels += invalidDepthOutsideVolumePixelsRow[y]; stats->totalValidPixelsDistance += validPixelDistanceRow[y]; } return S_OK; } /// /// Horizontally mirror a 32bit (color/float) image in-place. /// /// A pointer to the image to mirror. /// S_OK on success, otherwise failure code HRESULT HorizontalMirror32bitImageInPlace(const NUI_FUSION_IMAGE_FRAME *pImage) { if (nullptr == pImage || !(pImage->imageType == NUI_FUSION_IMAGE_TYPE_COLOR || pImage->imageType == NUI_FUSION_IMAGE_TYPE_FLOAT)) { return E_INVALIDARG; } if (nullptr == pImage->pFrameBuffer) { return E_NOINTERFACE; } unsigned int width = pImage->width; unsigned int height = pImage->height; if (0 == width || 0 == height || pImage->pFrameBuffer->Pitch == 0) { return E_INVALIDARG; } unsigned int *rawPixels = reinterpret_cast(pImage->pFrameBuffer->pBits); Concurrency::parallel_for(0u, height, [&](unsigned int y) { unsigned int index = y * width; unsigned int mirrorIndex = index + width - 1; for (unsigned int x = 0; x < (width / 2); ++x, ++index, --mirrorIndex) { // In-place swap to mirror unsigned int temp = rawPixels[index]; rawPixels[index] = rawPixels[mirrorIndex]; rawPixels[mirrorIndex] = temp; } }); return S_OK; } /// /// Horizontally mirror a 32bit (color/float) image. /// /// A pointer to the image to mirror. /// A pointer to the destination mirrored image. /// S_OK on success, otherwise failure code HRESULT HorizontalMirror32bitImage(const NUI_FUSION_IMAGE_FRAME *pSrcImage, const NUI_FUSION_IMAGE_FRAME *pDestImage) { if (nullptr == pSrcImage || nullptr == pDestImage) { return E_INVALIDARG; } if (nullptr == pSrcImage->pFrameBuffer || nullptr == pDestImage->pFrameBuffer) { return E_NOINTERFACE; } if (!(pSrcImage->imageType == NUI_FUSION_IMAGE_TYPE_FLOAT || pSrcImage->imageType == NUI_FUSION_IMAGE_TYPE_COLOR) || pSrcImage->imageType != pDestImage->imageType) { return E_INVALIDARG; } unsigned int width = pSrcImage->width; unsigned int height = pSrcImage->height; if (width != pDestImage->width || height != pDestImage->height) { return E_INVALIDARG; } if (pSrcImage->pFrameBuffer->Pitch == 0 || pDestImage->pFrameBuffer->Pitch == 0) { return E_INVALIDARG; } const unsigned int *pSrcBuffer = reinterpret_cast(pSrcImage->pFrameBuffer->pBits); unsigned int *pDestBuffer = reinterpret_cast(pDestImage->pFrameBuffer->pBits); Concurrency::parallel_for(0u, height, [&](unsigned int y) { const unsigned int *pSrcRow = reinterpret_cast(reinterpret_cast(pSrcBuffer) + (y * pSrcImage->pFrameBuffer->Pitch)); unsigned int *pDestRow = reinterpret_cast(reinterpret_cast(pDestBuffer) + (y * pDestImage->pFrameBuffer->Pitch)); for (unsigned int x = 0, flippedX = width-1; x < width; ++x, --flippedX) { pDestRow[flippedX] = pSrcRow[x]; } }); return S_OK; } /// /// Tests whether a resampling factor is valid. /// /// The resampling factor. /// true if a valid resampling factor, otherwise false. /// /// Valid resampling factors are powers of two between 1 and 16, inclusive. /// static inline bool IsValidResampleFactor(unsigned int factor) { return (1 == factor || 2 == factor || 4 == factor || 8 == factor || 16 == factor); } /// /// Down sample color frame with nearest neighbor to the depth frame resolution /// /// The source color image. /// The destination down sampled image. /// S_OK on success, otherwise failure code HRESULT DownsampleColorFrameToDepthResolution(NUI_FUSION_IMAGE_FRAME *src, NUI_FUSION_IMAGE_FRAME *dest) { if (nullptr == src || nullptr == dest) { return E_INVALIDARG; } if (src->imageType != NUI_FUSION_IMAGE_TYPE_COLOR || src->imageType != dest->imageType || src->width != 1920 || src->height != 1080 || dest->width != NUI_DEPTH_RAW_WIDTH || dest->height != NUI_DEPTH_RAW_HEIGHT) { return E_INVALIDARG; } NUI_FUSION_BUFFER *srcFrameBuffer = src->pFrameBuffer; NUI_FUSION_BUFFER *downsampledFloatFrameBuffer = dest->pFrameBuffer; float factor = 1080.0f / NUI_DEPTH_RAW_HEIGHT; // Make sure we've received valid data if (srcFrameBuffer->Pitch == 0 || downsampledFloatFrameBuffer->Pitch == 0) { return E_NOINTERFACE; } HRESULT hr = S_OK; float *srcValues = (float *)srcFrameBuffer->pBits; float *downsampledDestValues = (float *)downsampledFloatFrameBuffer->pBits; const unsigned int filledZeroMargin = 0; const unsigned int downsampledWidth = dest->width; const unsigned int srcImageWidth = src->width; ZeroMemory(downsampledDestValues, downsampledFloatFrameBuffer->Pitch * dest->height); Concurrency::parallel_for(filledZeroMargin, dest->height - filledZeroMargin, [=, &downsampledDestValues, &srcValues](unsigned int y) { unsigned int index = dest->width * y; for (unsigned int x=0; x < downsampledWidth; ++x, ++index) { int srcX = (int)(x * factor); int srcY = (int)(y * factor); int srcIndex = srcY * srcImageWidth + srcX; downsampledDestValues[index] = srcValues[srcIndex]; } }); return hr; } /// /// Down sample color, depth float or point cloud frame with nearest neighbor /// /// The source depth float or pointcloud image. /// The destination down sampled depth float or pointcloud image. /// The down sample factor (1=just copy, 2=x/2,y/2, 4=x/4,y/4). /// S_OK on success, otherwise failure code HRESULT DownsampleFrameNearestNeighbor(NUI_FUSION_IMAGE_FRAME *src, NUI_FUSION_IMAGE_FRAME *dest, unsigned int factor) { if (nullptr == src || nullptr == dest) { return E_INVALIDARG; } if (!(src->imageType == NUI_FUSION_IMAGE_TYPE_COLOR || src->imageType == NUI_FUSION_IMAGE_TYPE_FLOAT || src->imageType == NUI_FUSION_IMAGE_TYPE_POINT_CLOUD) || src->imageType != dest->imageType) { return E_INVALIDARG; } if (!IsValidResampleFactor(factor)) { return E_INVALIDARG; } NUI_FUSION_BUFFER *srcFrameBuffer = src->pFrameBuffer; NUI_FUSION_BUFFER *downsampledFloatFrameBuffer = dest->pFrameBuffer; unsigned int downsampledWidth = src->width / factor; unsigned int downsampleHeight = src->height / factor; if (1 == factor && srcFrameBuffer->Pitch * src->height != downsampledFloatFrameBuffer->Pitch * dest->height) { return E_INVALIDARG; } else if (dest->width != downsampledWidth || dest->height != downsampleHeight) { return E_INVALIDARG; } // Make sure we've received valid data if (srcFrameBuffer->Pitch == 0 || downsampledFloatFrameBuffer->Pitch == 0) { return E_NOINTERFACE; } HRESULT hr = S_OK; float *srcValues = (float *)srcFrameBuffer->pBits; float *downsampledDestValues = (float *)downsampledFloatFrameBuffer->pBits; const unsigned int srcImageWidth = src->width; if (1 == factor) { errno_t err = memcpy_s(downsampledDestValues, downsampledFloatFrameBuffer->Pitch * dest->height, srcValues, srcFrameBuffer->Pitch * src->height); if (0 != err) { hr = E_FAIL; } } else { // Adjust for point cloud image size (6 floats per pixel) unsigned int step = (src->imageType == NUI_FUSION_IMAGE_TYPE_POINT_CLOUD) ? 6 : 1; unsigned int factorStep = factor * step; Concurrency::parallel_for(0u, downsampleHeight, [=, &downsampledDestValues, &srcValues](unsigned int y) { unsigned int index = downsampledWidth * y * step; unsigned int srcIndex = srcImageWidth * y * factorStep; for (unsigned int x=0; x /// Up sample color or depth float (32bits/pixel) frame with nearest neighbor - replicates pixels /// /// The source color image. /// The destination up-sampled color image. /// The up sample factor (1=just copy, 2=x*2,y*2, 4=x*4,y*4). /// S_OK on success, otherwise failure code HRESULT UpsampleFrameNearestNeighbor(NUI_FUSION_IMAGE_FRAME *src, NUI_FUSION_IMAGE_FRAME *dest, unsigned int factor) { if (nullptr == src || nullptr == dest) { return E_INVALIDARG; } if (src->imageType != dest->imageType || !(src->imageType == NUI_FUSION_IMAGE_TYPE_COLOR || src->imageType == NUI_FUSION_IMAGE_TYPE_FLOAT)) { return E_INVALIDARG; } if (!IsValidResampleFactor(factor)) { return E_INVALIDARG; } NUI_FUSION_BUFFER *srcFrameBuffer = src->pFrameBuffer; NUI_FUSION_BUFFER *upsampledDestFrameBuffer = dest->pFrameBuffer; unsigned int upsampledWidth = src->width * factor; unsigned int upsampleHeight = src->height * factor; if (1 == factor && srcFrameBuffer->Pitch * src->height != upsampledDestFrameBuffer->Pitch * dest->height) { return E_INVALIDARG; } else if (dest->width != upsampledWidth || dest->height != upsampleHeight) { return E_INVALIDARG; } // Make sure we've received valid data if (srcFrameBuffer->Pitch == 0 || upsampledDestFrameBuffer->Pitch == 0) { return E_NOINTERFACE; } HRESULT hr = S_OK; unsigned int *srcValues = (unsigned int *)srcFrameBuffer->pBits; unsigned int *upsampledDestValues = (unsigned int *)upsampledDestFrameBuffer->pBits; const unsigned int srcImageWidth = src->width; const unsigned int srcImageHeight = src->height; if (1 == factor) { errno_t err = memcpy_s(upsampledDestValues, upsampledDestFrameBuffer->Pitch * dest->height, srcValues, srcFrameBuffer->Pitch * src->height); if (0 != err) { hr = E_FAIL; } } else { unsigned int upsampleRowMultiplier = upsampledWidth * factor; // Note we run this only for the source image height pixels to sparsely fill the destination with rows Concurrency::parallel_for(0u, srcImageHeight, [=, &upsampledDestValues, &srcValues](unsigned int y) { unsigned int index = upsampleRowMultiplier * y; unsigned int srcIndex = srcImageWidth * y; // Fill row for (unsigned int x=0; x