6 minutes read

We can transform an image by means of applying the same adjustment to every pixel of the image. Such transformations range from changing pixels' brightness to changing their color. In this topic, we'll learn about some popular pixel transformations.

Brightness

The brightness of a picture simply defines how dark or light it is. To change the brightness of an image, we change the value of each pixel color by a constant value. Adding a positive constant to all of the image color values increases the brightness of the image, while subtracting a positive constant from all of the image color values decreases the brightness.

Of course, these operations may sometimes lead to invalid color values. If the value is below 0, then you need to set it to 0, and if the value is above 255 (integer color representation), then set it to 255. In the case of float value representation, set values above 1.0 to 1.0.

Here is an example of changing the brightness of an image with a 24-bit color scheme. We read the color of each pixel and then change it, setting a new color for the pixel. The changes are applied to the original image.

fun changeBrightness(image: BufferedImage, offset: Int) {
    
    fun addOffset(colorValue: Int, offset: Int): Int {
        val newColorValue = colorValue + offset
        return if (newColorValue > 255) 255
            else if (newColorValue < 0) 0
            else newColorValue
    }

    for (x in 0 until image.width) {
        for (y in 0 until image.height) {
            val color = Color(image.getRGB(x, y))
            
            val colorNew = Color(
                addOffset(color.red, offset),
                addOffset(color.green, offset),
                addOffset(color.blue, offset))
            
            image.setRGB(x, y, colorNew.rgb)
        }
    }
}

Below are two images – the original and the new one with brightness changed by an offset of -50.

Two images – the original and the new one with brightness changed by an offset of -50

Contrast

The difference in brightness between different objects or regions of an image is referred to as contrast. To change the contrast of an image, we multiply the value of each pixel color by a constant value. If the value is greater than one, we increase the contrast. On the contrary, multiplying the value of all pixel colors by a number less than one, we decrease the contrast.

If the color value is greater than 255, then the value is set to 255 (to 1.0 for float representation).

Here is an example of a function that changes the contrast of an image with a 24-bit color scheme. It reads the color of each pixel and changes it, setting a new color to the pixel. The changes are applied to the original image.

fun changeContrast(image: BufferedImage, factor: Float) {

    fun multiplyWithFactor(colorValue: Int, factor: Float): Int {
        val newColorValue = (colorValue * factor).toInt()
        return if (newColorValue > 255) 255 else newColorValue
    }

    for (x in 0 until image.width) {
        for (y in 0 until image.height) {
            val color = Color(image.getRGB(x, y))

            val colorNew = Color(
                multiplyWithFactor(color.red, factor),
                multiplyWithFactor(color.green, factor),
                multiplyWithFactor(color.blue, factor))

            image.setRGB(x, y, colorNew.rgb)
        }
    }
}

Below are two images – the original and the one with its contrast changed by a factor of 0.85.

Two images – the original and the one with its contrast changed by a factor of 0.85

Class RescaleOp

The above code snippets for changing brightness and contrast are useful for education purposes, but their functionality is already covered by the RescaleOp class in the java.awt.image package. This class executes a pixel-by-pixel change by multiplying each color value by a factor and then adding an offset. That is, for each color value, it performs the newValue = (value * factor) + offset operation and then clips newValue to the appropriate minimum or maximum values.

The class may be imported as follows:

import java.awt.image.RescaleOp

Here is the simplest RescaleOp class constructor:

RescaleOp(factor: Float, offset: Float, hints: RenderingHints)

Here, hints is a java.awt.RenderingHints instance providing information on the algorithms to be used. In our case, we will set it to null.

Following is an example of creating a new BufferedImage named newImage from a BufferedImage named image by changing the brightness by an offset of 50. The member function filter(image: BufferedImage, newImage:BufferedImage) copies the changed image to newImage.

val offset = 50f

// Create a target BufferedImage
val newImage = BufferedImage(image.width, image.height, image.type)

// Create a RescaleOp instace with offset set to 50 and factor set to 1
val op = RescaleOp(1f, offset, null)

// Copy the image to newImage with changed brightness
op.filter(image, newImage)

Likewise, the contrast of an image can be changed as follows:

val scaleFactor = 1.5f

// Create a target BufferedImage
val newImage = BufferedImage(image.width, image.height, image.type)

// Create a RescaleOp instace with offset set to 0 and factor set to 1.5
val op = RescaleOp(scaleFactor, 0f, null)

// Copy the image to newImage with changed contrast
op.filter(image, newImage)

Color to Grayscale conversion

Grayscale images are entirely made up of grayscale shades. 24-bit or 32-bit color schemes can hold such images, with the same value for each color of a pixel. However, this is a huge waste of space. Grayscale images are typically saved in 8-bit grayscale color schemes. The value of 0 denotes black, the value of 255 denotes white (1.0 for float representation), and all intermediate values are various shades of gray. The BufferedImage class supports grayscale images with the TYPE_BYTE_GRAY (8-bit) and the TYPE_USHORT_GRAY (16-bit) types.

You can convert a color image to grayscale in many different ways, each producing a different result and different quality. It can be a simple but naive way – for example, setting a grayscale pixel value to the mean value of the 3 color values of that pixel. More sophisticated methods take into account the relative luminance and gamma correction of each pixel.

Here we are going to keep things simple and use the ColorConvertOp class of the java.awt.image package, together with the ColorSpace class of the java.awt.color package.

The ColorSpace class is used for holding the color space information, which in our case is the grayscale color space:

ColorSpace.getInstance(ColorSpace.CS_GRAY)

The ColorConvertOp class is used for the color conversion of the source image. We can get an instance of it with:

ColorConvertOp(cspace: ColorSpace, hints: RenderingHints)

Here hints will be set to null.

The conversion is performed with the help of the filter() member function of the ColorConvertOp class:

filter(src: BufferedImage, dest: BufferedImage)

Here, src is the source image and dest the destination image. The function returns the new image (if the return value is used, then dest can be set to null).

Take a look at an example of a function converting a color image to grayscale:

fun convertToGrayscale(image: BufferedImage): BufferedImage {
    // Create a grayscale ColorSpace Instance
    val cs = ColorSpace.getInstance(ColorSpace.CS_GRAY)

    // Create a ColorConvertOp instance
    val op = ColorConvertOp(cs, null)

    // Convert the image to grayscale
    val grayscale = op.filter(image, null)

    return grayscale
}

Below is an example of an image converted to grayscale.

Pixel transformation: colored imagePixels transformation: image converted to grayscale

Conclusion

We have introduced some basic pixel transformations, which you can find in any image editing program. However, this is only a starting point, since the subject of image transformation is huge and includes a great variety of possible image pixel conversions.

23 learners liked this piece of theory. 0 didn't like it. What about you?
Report a typo