Calcolo numerico per la generazione di immagini fotorealistiche
Maurizio Tomasi maurizio.tomasi@unimi.it
Today we will implement the code for our program
(main.py
in the Python version).
The program will function as follows:
$ ./main.py input_file.pfm 0.3 1.0 output_file.png
File 'input_file.pfm' has been read from disk
File 'output_file.png' has been written to disk
$
The goal is to convert a PFM file into a PNG file (or your
preferred LDR format). The values 0.3
and 1.0
refer to the scale
factor a and \gamma, respectively.
The tasks to be performed on the code are the following:
Color
object;HdrImage
;HdrImage
using a certain average luminosity, optionally
passed as an argument;main
function in the application
code.Let’s add simple luminosity
method to the
Color
class that uses Shirley & Morley’s formula (you
can implement others, if
you want):
If the equivalent of max
and min
in
your language only accepts two parameters (e.g., minOf
and
maxOf
in Kotlin), you can use the equivalence
\max\left\{a, b, c\right\} \equiv \max\bigl\{\max\left\{a, b\right\}, c\bigr\}.
Some tests for Color.luminosity
are also needed:
The pytest.approx()
method is part of the
pytest
library and corresponds to the is_close
function you implemented some time ago.
This is a trivial implementation of the equation we discussed during the last class:
def test_average_luminosity():
img = HdrImage(2, 1)
img.set_pixel(0, 0, Color( 5.0, 10.0, 15.0)) # Luminosity: 10.0
img.set_pixel(1, 0, Color(500.0, 1000.0, 1500.0)) # Luminosity: 1000.0
# We pass delta=0.0 to avoid roundings
print(img.average_luminosity(delta=0.0))
assert pytest.approx(100.0) == img.average_luminosity(delta=0.0)
# The test above does not verify that delta prevents crashes when
# there are black pixels! Fix this by adding a new test
The normalize_image
function calculates the average
brightness of an image according to the corresponding
equation.
The function should accept the value of a as an input parameter (in
factor
):
It’s better to ask for the luminosity instead of calculating it:
If your language supports optional types, you can call the
function average_luminosity
if the brightness parameter is
null:
def test_normalize_image():
img = HdrImage(2, 1)
img.set_pixel(0, 0, Color( 5.0, 10.0, 15.0))
img.set_pixel(1, 0, Color(500.0, 1000.0, 1500.0))
img.normalize_image(factor=1000.0, luminosity=100.0)
assert img.get_pixel(0, 0).is_close(Color(0.5e2, 1.0e2, 1.5e2))
assert img.get_pixel(1, 0).is_close(Color(0.5e4, 1.0e4, 1.5e4))
We must implement the function that clips too large values for R, G, B, according to the equation shown during the last class.
def test_clamp_image():
img = HdrImage(2, 1)
img.set_pixel(0, 0, Color(0.5e1, 1.0e1, 1.5e1))
img.set_pixel(1, 0, Color(0.5e3, 1.0e3, 1.5e3))
img.clamp_image()
# Just check that the R/G/B values are within the expected boundaries
for cur_pixel in img.pixels:
assert (cur_pixel.r >= 0) and (cur_pixel.r <= 1)
assert (cur_pixel.g >= 0) and (cur_pixel.g <= 1)
assert (cur_pixel.b >= 0) and (cur_pixel.b <= 1)
This kind of library is usually installed using sudo
in some way, such as
./configure && make && sudo make install
.
Example: installing ROOT on your system to work on TNDS exercises!
However, it often happens that you have to use incompatible
libraries in your code! Suppose we have numerous oscilloscopes in an
electronics lab, and we use the oscilloscope
library to
analyze their measurements:
So far we have used version 2.4 to write a lot of analysis code that we use regularly.
However, version 3.0 of oscilloscope
has been out
for a while now, which has interesting new features, but I haven’t
upgraded yet because it’s incompatible with version 2.4!
Version 4.0 is about to be released, even more powerful, but it will no longer support one of my old oscilloscopes, which I still need.
Local libraries solve these problems because they are not installed system-wide, but are linked to a single repository:
Example: copy of a header-only library into your C++ project.
Example: virtual environment in Python.
Almost all languages support “local” library management.
These features are usually implemented in the programs you have
used so far to create projects (nimble
,
gradle
, dotnet
, dub
,
cargo
…).
Your task for today is to choose a library that supports
writing LDR images and to import it into your project as a
local dependency (no sudo
!).
The use of local libraries will help us a lot when we deal with continuous integration.
normalize_image
and clamp_image
have
been applied, all RGB components of the colors in the matrix will be in
the range [0, 1].Language | Libraries |
---|---|
C# | ImageSharp |
D | imageformats |
Java/Kotlin | javax.imageio (già installato) |
Nim | simplepng |
Python | Pillow |
Rust | image |
You can search for other libraries, but be sure to pick a pixel-based one!
Choose an LDR library that has these characteristics:
width
) and the number of rows (height
)x
and
y
coordinates(0, 0)
at the top left? Bottom left? Or…?class HdrImage:
# ...
def write_ldr_image(self, stream, format, gamma=1.0):
from PIL import Image
img = Image.new("RGB", (self.width, self.height))
for y in range(self.height):
for x in range(self.width):
cur_color = self.get_pixel(x, y)
img.putpixel(xy=(x, y), value=(
int(255 * math.pow(cur_color.r, 1 / gamma)),
int(255 * math.pow(cur_color.g, 1 / gamma)),
int(255 * math.pow(cur_color.b, 1 / gamma)),
))
img.save(stream, format=format)
main
function (1/2)from dataclasses import dataclass
@dataclass
class Parameters:
input_pfm_file_name: str = ""
factor:float = 0.2
gamma:float = 1.0
output_png_file_name: str = ""
def parse_command_line(self, argv):
if len(sys.argv) != 5:
raise RuntimeError("Usage: main.py INPUT_PFM_FILE FACTOR GAMMA OUTPUT_PNG_FILE")
self.input_pfm_file_name = sys.argv[1]
try:
self.factor = float(sys.argv[2])
except ValueError:
raise RuntimeError(f"Invalid factor ('{sys.argv[2]}'), it must be a floating-point number.")
try:
self.gamma = float(sys.argv[3])
except ValueError:
raise RuntimeError(f"Invalid gamma ('{sys.argv[3]}'), it must be a floating-point number.")
self.output_png_file_name = sys.argv[4]
(Instead of using global variables for the parameters read from
argv
, I use a struct
and pass it to other
functions: it’s easier to write tests!)
main
function (2/2)def main(argv):
parameters = Parameters()
try:
parameters.parse_command_line(argv)
except RuntimeError as err:
print("Error: ", err)
return
# You should put a try…except block here and produce a nice error
# message is the image couldn't be saved
with open(parameters.input_pfm_file_name, "rb") as inpf:
img = hdrimages.read_pfm_image(inpf)
print(f'File "{parameters.input_pfm_file_name}" has been read from disk.')
img.normalize_image(factor=parameters.factor)
img.clamp_image()
# Same as above: use try…except to produce a human-readable message
# if something goes wrong
with open(parameters.output_png_file_name, "wb") as outf:
img.write_ldr_image(stream=outf, format="PNG", gamma=parameters.gamma)
print(f'File "{parameters.output_png_file_name}" has been written to disk.')
Color
object and the average luminosity of an
HdrImage
and add tests;HdrImage
using a certain average luminosity, optionally
passed as an argument and add tests;main
function in the application code, so
that it accepts 4 arguments: the PFM file to read, the value of a, the value of γ, and the name of the
PNG/JPEG/etc. file to create.If you need a realistic PFM image, you can use memorial.pfm.
There is also the site Scenes for pbrt-v3.
Awesome C++ is a treasure trove of C++ libraries. Have a look at the section Image processing to find a viable image processing library to use in your project.
To document code, the most used tool is Doxygen, but there are other choices available (See again the section “Documentation” in the Awesome C++ website.)
Image
type.Using Pkg.add
, install both Images
and
ImageIO
in your package.
Just create matrices of RGB
values and save them
with the save
command; the file extension determines its
format:
The ImageSharp library supports many formats: JPEG, PNG, BMP, GIF, and TGA (avoid the latter if you can, as it is very old and provides no compression).
In C#, you can automatically download and install libraries and
specify that they should be used in your projects without the need to
modify Makefiles or use root-config
,
pkg-config
, or similar tools.
Add the SixLabors.ImageSharp
package to the class library (which you may have named
Tracer
):
$ dotnet add package SixLabors.ImageSharp
// Create a sRGB bitmap
var bitmap = new Image<Rgb24>(Configuration.Default, width, height);
// The bitmap can be used as a matrix. To draw the pixels in the bitmap
// just use the syntax "bitmap[x, y]" like the following:
bitmap[SOMEX, SOMEY] = new Rgb24(255, 255, 128); // Three "Byte" values!
// Save the bitmap as a PNG file
using (Stream fileStream = File.OpenWrite("output.png")) {
bitmap.Save(fileStream, new PngEncoder());
}
You can install libraries in your project with your package manager:
nimble install NAME
;cargo
following the
guide;dub add NAME
.These libraries will be installed as dependencies of your program, and not system-wide.
Unlike Python, C++, C# and Julia, both Java and Kotlin provide support for PNG, JPEG, BMP and GIF images through the standard Java libraries.
You need the classes java.awt.image.BufferedImage
(LDR image) and javax.imageio.ImageIO
(the class that
implements the methods for reading/writing LDR images to
files).
The BufferedImage
class allows you to encode color
in many different ways. If you use TYPE_INT_RGB
, the color
is not an RGB triplet but a 32-bit integer with this format:
00000000 rrrrrrrr gggggggg bbbbbbbb
where r
are the bits for red, g
for green
and b
for blue. Usually colors are indicated using
hexadecimal notation, because this way they are always six digits,
e.g. 0x12FA51
.
If r
, g
and b
are bytes in
the range [0, 255], you can use one of these two formulas:
r * 65536 + g * 256 + b (r shl 16) + (g shl 8) + b
fun main(args: Array<String>) {
val width = 800
val height = 600
val ldrImage = BufferedImage(width, height, BufferedImage.TYPE_INT_RGB)
for (y in 0 until height) {
for (x in 0 until width) {
// 0xFF0000 corresponds to sRGB(255, 0, 0): the image will be
// painted uniformly with a bright red shade
ldrImage.setRGB(x, y, 0xFF0000)
}
}
// Save the image to the file specified on the command line
ImageIO.write(args[0], "PNG", stream)
}
Java and Kotlin have a somewhat «baroque» way of handling command line parameters.
Your executable cannot be run like a normal Python/C++/C# executable:
$ ./main.py input_file.pfm 0.3 1.0 output_file.png
because it is compiled for the JVM.
If you are using Kotlin, you have to go through
gradlew
, which requires that parameters be passed through
--args
:
./gradlew run --args="input_file.pfm 0.3 1.0 output_file.png"