Introduce mipgen frontend. (#120)

* Introduce mipgen frontend.

For now this is fairly simple although it does let you choose a filter.
Another cool feature is that an HTML file is generated which makes it
easy to review the generated miplevels.

This does not expose the Boundary enum to users because it has not yet
been implemented in the core Image library.

* Fix up mipgen per code review.
This commit is contained in:
Philip Rideout
2018-08-21 17:54:27 -07:00
committed by GitHub
parent b587ec3f43
commit e45e7b256d
29 changed files with 429 additions and 0 deletions

View File

@@ -288,6 +288,7 @@ if (NOT ANDROID)
add_subdirectory(${TOOLS}/filamesh)
add_subdirectory(${TOOLS}/matc)
add_subdirectory(${TOOLS}/matinfo)
add_subdirectory(${TOOLS}/mipgen)
add_subdirectory(${TOOLS}/normal-blending)
add_subdirectory(${TOOLS}/roughness-prefilter)
add_subdirectory(${TOOLS}/skygen)

View File

@@ -115,6 +115,7 @@ Many other features have been either prototyped or planned:
- `filamesh`: Mesh converter
- `matc`: Material compiler
- `matinfo` Displays information about materials compiled with `matc`
- `mipgen` Generates a series of miplevels from a source image.
- `normal-blending`: Tool to blend normal maps
- `roughness-prefilter`: Pre-filters a roughness map from a normal map to reduce aliasing
- `skygen`: Physically-based sky environment texture generator

View File

@@ -129,6 +129,28 @@ LinearImage resampleImage(const LinearImage& source, uint32_t width, uint32_t he
void computeSingleSample(const LinearImage& source, float x, float y, SingleSample* result,
Filter filter = Filter::BOX);
/**
* Generates a sequence of miplevels using the requested filter. To determine the number of mips
* it would take to get down to 1x1, see getMipmapCount.
*
* Source image need not be power-of-two. In the result vector, the half-size image is returned at
* index 0, the quarter-size image is at index 1, etc. Please note that the original-sized image is
* not included.
*/
void generateMipmaps(const LinearImage& source, Filter, LinearImage* result, uint32_t mipCount);
/**
* Returns the number of miplevels it would take to downsample the given image down to 1x1. This
* number does not include the original image (i.e. mip 0).
*/
uint32_t getMipmapCount(const LinearImage& source);
/**
* Given the string name of a filter, converts it to uppercase and returns the corresponding
* enum value. If no corresponding enumerant exists, returns DEFAULT.
*/
Filter filterFromString(const char* name);
} // namespace image
#endif /* IMAGE_IMAGESAMPLER_H */

View File

@@ -19,9 +19,11 @@
#include <math/vec3.h>
#include <utils/Panic.h>
#include <utils/CString.h>
#include <memory>
#include <vector>
#include <unordered_map>
using namespace image;
@@ -320,4 +322,48 @@ void computeSingleSample(const LinearImage& source, float x, float y, SingleSamp
}
}
// Unlike traditional mipmap generation, our implementation generates all levels from the original
// image, under the premise that this produces a higher quality result.
void generateMipmaps(const LinearImage& source, Filter filter, LinearImage* result, uint32_t mips) {
mips = std::min(mips, getMipmapCount(source));
uint32_t width = source.getWidth();
uint32_t height = source.getHeight();
for (uint32_t n = 0; n < mips; ++n) {
width = std::max(width >> 1, 1u);
height = std::max(height >> 1, 1u);
result[n] = resampleImage(source, width, height, filter);
}
}
uint32_t getMipmapCount(const LinearImage& source) {
uint32_t width = source.getWidth();
uint32_t height = source.getHeight();
uint32_t count = 0;
while (width > 1 || height > 1) {
++count;
width = std::max(width >> 1, 1u);
height = std::max(height >> 1, 1u);
}
return count;
}
Filter filterFromString(const char* rawname) {
using namespace utils;
using namespace std;
static const unordered_map<StaticString, Filter> map = {
{ "BOX", Filter::BOX},
{ "NEAREST", Filter::NEAREST},
{ "HERMITE", Filter::HERMITE},
{ "GAUSSIAN", Filter::GAUSSIAN_SCALARS},
{ "NORMALS", Filter::GAUSSIAN_NORMALS},
{ "MITCHELL", Filter::MITCHELL},
{ "LANCZOS", Filter::LANCZOS},
{ "MINIMUM", Filter::MINIMUM},
};
string name = rawname;
for (auto& c: name) c = toupper((unsigned char) c);
auto iter = map.find({ name.c_str(), name.size() });
return iter == map.end() ? Filter::DEFAULT : iter->second;
}
} // namespace image

View File

@@ -0,0 +1,47 @@
<!DOCTYPE html>
<html>
<head>
<title>test_image</title>
<style>
img {
image-rendering: pixelated;
border: solid 2px;
}
p.horizontal { font-size: 0 }
.horizontal img { border-left: 0; }
.horizontal img:first-of-type { border-left: solid 2px; }
.vertical img { border-top: 0; display: block }
.vertical img:first-of-type { border-top: solid 2px; }
</style>
</head>
<body>
<p>
Modern CSS lets you specify nearest-neighbor scaling, so a nice way of examining the results from
our filtering tests is this tiny web page. It can be refreshed at the touch of a button. Moreover
Chrome respects the color space attribute of PNG images, which cannot be said of many image viewers.
You can also use the <a href="http://www.colorzilla.com/chrome/">ColorZilla</a>
extension to scrub over color values.
</p>
<p class="horizontal">
<img src="mip0_5x10.png" width="200px" height="400px">
<img src="mip1_5x10.png" width="200px" height="400px">
<img src="mip2_5x10.png" width="200px" height="400px">
<img src="mip3_5x10.png" width="200px" height="400px">
</p>
<p class="vertical">
<img src="mip0_200x100.png" width="100px" height="50px">
<img src="mip1_200x100.png" width="100px" height="50px">
<img src="mip2_200x100.png" width="100px" height="50px">
<img src="mip3_200x100.png" width="100px" height="50px">
<img src="mip4_200x100.png" width="100px" height="50px">
<img src="mip5_200x100.png" width="100px" height="50px">
<img src="mip6_200x100.png" width="100px" height="50px">
<img src="mip7_200x100.png" width="100px" height="50px">
</p>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 487 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 346 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 B

View File

@@ -278,6 +278,35 @@ TEST_F(ImageTest, ColorTransformRGBA) { // NOLINT
ASSERT_NEAR(pixels[3].w, 0.99183642f, 0.001f);
}
TEST_F(ImageTest, Mipmaps) { // NOLINT
Filter filter = filterFromString("HERMITE");
ASSERT_EQ(filter, Filter::HERMITE);
// Miplevels: 5x10, 2x5, 1x2, 1x1.
LinearImage src = createColorFromAscii(
"44444 41014 40704 41014 44444 44444 41014 40704 41014 44444");
uint32_t count = getMipmapCount(src);
ASSERT_EQ(count, 3);
std::vector<LinearImage> mips(count);
generateMipmaps(src, filter, mips.data(), count);
updateOrCompare(src, "mip0_5x10.png");
for (uint32_t index = 0; index < count; ++index) {
updateOrCompare(mips[index], "mip" + std::to_string(index + 1) + "_5x10.png");
}
// Test color space with a classic RED => GREEN color gradient.
src = createColorFromAscii("12");
src = resampleImage(src, 200, 100, Filter::NEAREST);
count = getMipmapCount(src);
ASSERT_EQ(count, 7);
mips.resize(count);
generateMipmaps(src, filter, mips.data(), count);
updateOrCompare(src, "mip0_200x100.png");
for (uint32_t index = 0; index < count; ++index) {
updateOrCompare(mips[index], "mip" + std::to_string(index + 1) + "_200x100.png");
}
}
static void printUsage(const char* name) {
string exec_name(utils::Path(name).getName());
string usage(

View File

@@ -0,0 +1,28 @@
cmake_minimum_required(VERSION 3.1)
project(mipgen)
set(TARGET mipgen)
# ==================================================================================================
# Source files
# ==================================================================================================
set(SRCS src/main.cpp)
# ==================================================================================================
# Target definitions
# ==================================================================================================
add_executable(${TARGET} ${SRCS})
target_link_libraries(${TARGET} PRIVATE math utils z image imageio getopt)
# =================================================================================================
# Licenses
# ==================================================================================================
set(MODULE_LICENSES getopt libpng tinyexr libz)
set(GENERATION_ROOT ${CMAKE_CURRENT_BINARY_DIR}/generated)
list_licenses(${GENERATION_ROOT}/licenses/licenses.inc ${MODULE_LICENSES})
target_include_directories(${TARGET} PRIVATE ${GENERATION_ROOT})
# ==================================================================================================
# Installation
# ==================================================================================================
install(TARGETS ${TARGET} RUNTIME DESTINATION bin)

255
tools/mipgen/src/main.cpp Normal file
View File

@@ -0,0 +1,255 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <image/ImageSampler.h>
#include <image/LinearImage.h>
#include <imageio/ImageDecoder.h>
#include <imageio/ImageEncoder.h>
#include <utils/Path.h>
#include <getopt/getopt.h>
#include <fstream>
#include <iostream>
using namespace image;
using namespace std;
using namespace utils;
static ImageEncoder::Format g_format = ImageEncoder::Format::PNG_LINEAR;
static bool g_formatSpecified = false;
static bool g_createGallery = false;
static string g_compression = "";
static Filter g_filter = Filter::DEFAULT;
static const char* USAGE = R"TXT(
MIPGEN generates mipmaps for an image down to the 1x1 level.
Output filenames are generated using the specified printf pattern.
For example, "mip%2d.png" would generate mip01.png, mip02.png, etc.
Note that miplevel 0 is not generated since it is the original image.
Usage:
MIPGEN [options] <input_file> <output_pattern>
Options:
--help, -h
print this message
--license
print copyright and license information
--gallery, -g
generate HTML gallery for review purposes (mipmap.html)
--format=[exr|hdr|rgbm|psd|png|dds], -f [exr|hdr|rgbm|psd|png|dds]
specify output file format, inferred from output pattern if omitted
--kernel=[box|nearest|hermite|gaussian|normals|mitchell|lanczos|min], -k [filter]
specify filter kernel type (defaults to LANCZOS)
--compression=COMPRESSION, -c COMPRESSION
format specific compression:
PNG: Ignored
Radiance: Ignored
Photoshop: 16 (default), 32
OpenEXR: RAW, RLE, ZIPS, ZIP, PIZ (default)
DDS: 8, 16 (default), 32
Example:
MIPGEN -g --kernel=hermite grassland.png mip_%03d.png
)TXT";
static const char* HTML_PREFIX = R"HTML(<!DOCTYPE html>
<html>
<head>
<style>
img {
image-rendering: pixelated;
border: solid 2px;
padding: 2px;
display: block;
}
</style>
</head>
<body>
)HTML";
static const char* HTML_SUFFIX = R"HTML(</body>
</html>
)HTML";
static void printUsage(const char* name) {
string execName(Path(name).getName());
const string from("MIPGEN");
string usage(USAGE);
for (size_t pos = usage.find(from); pos != string::npos; pos = usage.find(from, pos)) {
usage.replace(pos, from.length(), execName);
}
puts(usage.c_str());
}
static void license() {
cout <<
#include "licenses/licenses.inc"
;
}
static int handleArguments(int argc, char* argv[]) {
static constexpr const char* OPTSTR = "hlgf:c:k:";
static const struct option OPTIONS[] = {
{ "help", no_argument, 0, 'h' },
{ "license", no_argument, 0, 'l' },
{ "gallery", no_argument, 0, 'g' },
{ "format", required_argument, 0, 'f' },
{ "compression", required_argument, 0, 'c' },
{ "kernel", required_argument, 0, 'k' },
{ 0, 0, 0, 0 } // termination of the option list
};
int opt;
int optionIndex = 0;
while ((opt = getopt_long(argc, argv, OPTSTR, OPTIONS, &optionIndex)) >= 0) {
string arg(optarg ? optarg : "");
switch (opt) {
default:
case 'h':
printUsage(argv[0]);
exit(0);
case 'l':
license();
exit(0);
case 'g':
g_createGallery = true;
break;
case 'k': {
bool isvalid;
g_filter = filterFromString(arg.c_str());
if (g_filter == Filter::DEFAULT) {
cerr << "Warning: unrecognized filter, falling back to DEFAULT." << endl;
}
break;
}
case 'f':
if (arg == "png") {
g_format = ImageEncoder::Format::PNG;
g_formatSpecified = true;
}
if (arg == "hdr") {
g_format = ImageEncoder::Format::HDR;
g_formatSpecified = true;
}
if (arg == "rgbm") {
g_format = ImageEncoder::Format::RGBM;
g_formatSpecified = true;
}
if (arg == "exr") {
g_format = ImageEncoder::Format::EXR;
g_formatSpecified = true;
}
if (arg == "psd") {
g_format = ImageEncoder::Format::PSD;
g_formatSpecified = true;
}
if (arg == "dds") {
g_format = ImageEncoder::Format::DDS_LINEAR;
g_formatSpecified = true;
}
break;
case 'c':
g_compression = arg;
break;
}
}
return optind;
}
int main(int argc, char* argv[]) {
int optionIndex = handleArguments(argc, argv);
int numArgs = argc - optionIndex;
if (numArgs < 2) {
printUsage(argv[0]);
return 1;
}
Path inputPath(argv[optionIndex++]);
string outputPattern(argv[optionIndex]);
if (!g_formatSpecified) {
constexpr bool forceLinear = true;
g_format = ImageEncoder::chooseFormat(outputPattern, forceLinear);
}
puts("Reading image...");
ifstream inputStream(inputPath.getPath(), ios::binary);
LinearImage sourceImage = ImageDecoder::decode(inputStream, inputPath.getPath());
if (!sourceImage.isValid()) {
cerr << "Unable to open image: " << inputPath.getPath() << endl;
exit(1);
}
puts("Generating miplevels...");
uint32_t count = getMipmapCount(sourceImage);
vector<LinearImage> miplevels(count);
generateMipmaps(sourceImage, g_filter, miplevels.data(), count);
puts("Writing image files to disk...");
char path[256];
uint32_t mip = 1; // start at 1 because 0 is the original image
for (auto image: miplevels) {
int result = snprintf(path, sizeof(path), outputPattern.c_str(), mip++);
if (result < 0 || result >= sizeof(path)) {
cerr << "Output pattern is too long." << endl;
exit(1);
}
ofstream outputStream(path, ios::binary | ios::trunc);
if (!outputStream) {
cerr << "The output file cannot be opened: " << path << endl;
} else {
ImageEncoder::encode(outputStream, g_format, image, g_compression, path);
outputStream.close();
if (!outputStream) {
cerr << "An error occurred while writing the output file: " << path << endl;
}
}
}
if (g_createGallery) {
puts("Generating mipmaps.html...");
char tag[256];
mip = 1;
const char* pattern = R"(<image src="%s" width="%dpx" height="%dpx">)";
const uint32_t width = sourceImage.getWidth();
const uint32_t height = sourceImage.getHeight();
ofstream html("mipmaps.html", ios::trunc);
html << HTML_PREFIX;
int result = snprintf(tag, sizeof(tag), pattern, inputPath.c_str(), width, height);
if (result < 0 || result >= sizeof(tag)) {
cerr << "Output pattern is too long." << endl;
exit(1);
}
html << tag << std::endl;
for (auto image: miplevels) {
snprintf(path, sizeof(path), outputPattern.c_str(), mip++);
result = snprintf(tag, sizeof(tag), pattern, path, width, height);
if (result < 0 || result >= sizeof(tag)) {
cerr << "Output pattern is too long." << endl;
exit(1);
}
html << tag << std::endl;
}
html << HTML_SUFFIX;
}
puts("Done.");
}