MEDIA GUIDES / Image Effects

Python Create Image: A Practical Guide for Beginners and Pros

Images are a core part of how applications communicate information, from charts and visual reports to dynamic media and graphics. Developers often need to generate images programmatically rather than rely on static files. Python makes this possible with libraries that support drawing, compositing, and exporting images with just a few lines of code.

Creating images in Python is useful for data visualization, automated content generation, testing, and backend media workflows. You can produce everything from simple shapes to complex visuals and then store or deliver them as part of an application. Learning how to generate images programmatically helps teams build flexible systems that adapt quickly to changing requirements.

In this article:

What It Means to Create an Image in Python

Images in Python are represented by a multi-dimensional array of values, these values carry the pixel data which reflect things like coloring, transparency, and maybe other pixel properties.

Creating an image in Python is the process of generating these values in memory using Python code. We can then view their corresponding visual representation of the image on the screen, or save it as an image file on persistent storage.

This image creation process is typically implemented through a Python library, which hides any underlying complexities and presents a simple interface that includes a set of objects and methods we can interact with to create and manipulate images.

Best Libraries for Creating Images in Python

Python has massive support for image processing and manipulation, and there are many libraries that are actively maintained and continuously improved to work with images. We’ll explore the most common libraries and see how to use each of them to create an image.

OpenCV

OpenCV represents images as a NumPy array, the dimensions of the array depend on whether the image is colored (3D array) or grayscale (2D array). The first two dimensions of the array are the height and width of the image, if the image is grayscale, then the value of each index in the array is the gray level of that pixel, which is an integer between 0 and 255.

If the image is colored, the third dimension of the array will represent the 3 color channels, or 4 channels in case of an alpha channel for transparency. Color channels in OpenCV follow the BGR format, which is unlike most other libraries that use RGB.

Now enough for the theory, let’s see how to use OpenCV in practice to create an image.

First you’ll need to install the OpenCV and NumPy libraries with pip install opencv-python

This command will install the OpenCV library which will automatically include NumPy. We can then create an image and display it with the following code:

import cv2
import numpy as np

height, width = 200, 200

black_image = np.zeros((height, width, 3), dtype=np.uint8)

cv2.imshow('Display Window', black_image)

cv2.waitKey(0)

cv2.destroyAllWindows()

Here, we:

  • Create a new image with 200*200 dimensions.
  • The np.zeros() method creates an array with all the values set to zero, and the data type of each element in the array is uint8. This will set the BGR values to zeros, creating a black image.
  • Then we display it using the imshow() method:

We can manipulate this image by modifying the values of the created array. For example, we can change the image color by setting the desired BGR color values:

import cv2
import numpy as np

height, width = 200, 200

black_image = np.zeros((height, width, 3), dtype=np.uint8)

blue_image = black_image

for y in range(height):
    for x in range(width):
        blue_image[x, y, 0] = 255

cv2.imshow('Display Window', blue_image)

cv2.waitKey(0)

cv2.destroyAllWindows()

Pillow

Pillow represents images a bit different than OpenCV. In Pillow, each image is a grid of two dimensional array where each element in the array represents a pixel. The image information is stored as part of an Image object.

This Image object also has an attribute that represents a specific image “mode”. The most common modes are RGB for colored images, RGBA images with an alpha channel for transparency, and L or luminous mode for grayscale images. The image mode decides how each pixel information is represented.

For example, in RGB mode, a pixel is represented by a 3-tuple with each value in the tuple corresponding to the red, green, or blue color level. In the RGBA mode the pixel is a 4-tuple with the fourth element representing the alpha channel or transparency level. In the L mode, a pixel is represented by only an integer value that corresponds to the luminous degree of the pixel.

To use the Pillow library, you first need to install it via pip install Pillow

Then you can create an image with the Image.new() method as follows:

from PIL import Image

white_img = Image.new("RGB", (200, 200), (255, 255, 255))

white_img.show()

As we can see, creating a basic image with Pillow is relatively simple with few code lines. The Image.new() method takes as a parameter the image mode, the image dimensions as a 2-tuple, and optionally the pixel color values you want to set for the image. Because we’re using an RGB mode, the pixel color is provided as a 3-tuple. The 255 value for RGB means all the pixels will have the white color:

We can also modify the image color by setting a different 3-tuple value for all the pixels using the putpixel() method:

from PIL import Image

white_img = Image.new("RGB", (200, 200), (255, 255, 255))

red_color = (255, 0, 0)

red_img = white_img

for x in range(200):
    for y in range(200):
        red_img.putpixel((x, y), red_color)

red_img.show()

Matplotlib

The Matplotlib represents images similar to OpenCV. It uses NumPy arrays for storing the pixel information.

A grayscale image is represented as a 2D array where each element is a single integer value between 0 and 255 that corresponds to the luminous degree of that pixel. A colored image is represented as a 3D array, where the third dimension is the image channels which can be RGB or RGBA. Matplotlib can represent pixel values not only as integers between 0 and 255, but also as floats ranging from 0.0 to 1.0.

As usual, start with installing the Matplotlib and NumPy libraries with pip install matplotlib numpy

Then you can create an image by initializing a NumPy array and using its data:

import numpy as np
import matplotlib.pyplot as plt

image_height = 200
image_width = 200

my_image = np.zeros((image_height, image_width), dtype=np.uint8)

for x in range(image_width):
    for y in range(image_height):
        my_image[x][y] = 123

plt.imshow(my_image, cmap='gray', vmin=0, vmax=255)
plt.axis('off')
plt.show()
plt.close()

Here we create a 2D array for a grayscale image. We initialize the array values with zeros using the np.zeros() method, then we modify the values and set it to 123, which corresponds to the gray level color of each pixel:

We can change it to a colored image by adding a third dimension when creating the NumPy array and set the color level of each channel (RGB) in this dimension:

import numpy as np
import matplotlib.pyplot as plt

image_height = 200
image_width = 200

my_image = np.zeros((image_height, image_width, 3), dtype=np.uint8)

for x in range(image_width):
    for y in range(image_height):
        my_image[x][y][0] = 255

plt.imshow(my_image)
plt.axis('off')
plt.show()
plt.close()

Drawing Shapes, Text, and Gradients

After we’ve created an image, let’s see how we can draw other objects and display them on the screen. Using the Python libraries we covered before, we can create shapes, text, and apply color gradients. For our examples, we’ll use the OpenCV library.

Draw Shapes

We’ll start by drawing a simple line shape. First we need to create a colored image, then we’ll specify the shape type and properties to be drawn within this image space:

import cv2
import numpy as np

img = np.zeros((300, 300, 3), dtype=np.uint8)

cv2.line(img, (0, 0), (299, 299), (255, 255, 255), 5)

cv2.imshow("image-window", img)
cv2.waitKey(0)
cv2.destroyAllWindows()

In the above code, we:

  • Use the cv2.line() method to specify the shape type as a line.
  • Provide the parameters for:
    • Which image space it will be drawn inside
    • Starting and ending coordinates
    • Color in BGR format
    • Thickness.

The information we provided here will draw a diagonal line that starts from the top-left of the image (0, 0) and ends at the bottom-right (0, 0) with a white color (255, 255, 255):

And we can manipulate the background image and line values to change how it looks:

import cv2
import numpy as np

img = np.zeros((300, 300, 3), dtype=np.uint8)

img.fill(255)

cv2.line(img, (0, 0), (299, 299), (0, 255, 0), 10)

cv2.imshow("image-window", img)
cv2.waitKey(0)
cv2.destroyAllWindows()

Next, let’s draw a rectangle shape. This time we’ll use cv2.rectangle() method as follows:

import cv2
import numpy as np

img = np.zeros((300, 300, 3), dtype=np.uint8)

cv2.rectangle(img, (30, 50), (269, 150), (255, 0, 0), 5)

cv2.imshow("image-window", img)
cv2.waitKey(0)
cv2.destroyAllWindows()

The cv2.rectangle() method takes the same parameters as the previous cv2.line() method, with the starting point referring to the top-left corner of the rectangle, and the ending point referring to the bottom-right corner:

Also if you set the thickness value to -1 it will fill the shape with the provided color:

import cv2
import numpy as np

img = np.zeros((300, 300, 3), dtype=np.uint8)

cv2.rectangle(img, (30, 50), (269, 150), (255, 0, 0), -1)

cv2.imshow("image-window", img)
cv2.waitKey(0)
cv2.destroyAllWindows()

OpenCV also has a method for drawing circle shapes through cv2.circle(). It takes the same parameters as the previous shapes’ methods, with the starting coordinate representing the circle center, and the ending coordinate replaced by an integer that represents the circle radius:

import cv2
import numpy as np

img = np.zeros((300, 300, 3), dtype=np.uint8)

cv2.circle(img, (149, 149), 30 , (0, 0, 255), -1)

cv2.imshow("image-window", img)
cv2.waitKey(0)
cv2.destroyAllWindows()

Add Text

OpenCV also makes it easy to draw text on the screen. It provides a method for doing this called cv2.putText(). We create an image as a background then we use this method to draw text on this image space as follows:

import cv2
import numpy as np

img = np.zeros((300, 300, 3), dtype=np.uint8)

cv2.putText(img, "Hello World!", (15, 165), cv2.FONT_HERSHEY_SIMPLEX, 1.5, (255, 0, 255), 5)

cv2.imshow("image-window", img)
cv2.waitKey(0)
cv2.destroyAllWindows()

The parameters that cv2.putText() method takes are:

  • Image name
  • Text to draw
  • Coordinates of the org (which represents the bottom-left point of a rectangle that holds the text)
  • Font type (which OpenCV provides some basic built-in options)
  • Font scale
  • Font color
  • Thickness 

Apply Gradients

OpenCV doesn’t provide a built-in method for applying gradients. But we can manually implement it by changing the pixel color intensity and control the gradient direction using the following code:

import cv2
import numpy as np

img = np.zeros((300, 500, 3), dtype=np.uint8)

for x in range(300):
    interp = x / (299)
    color = [
        int((0, 0, 0)[i] * (1 - interp) + (255, 255, 255)[i] * interp)
        for i in range(3)
    ]
    img[:, x] = color

cv2.imshow("image-window", img)
cv2.waitKey(0)
cv2.destroyAllWindows()

Here we’re making the gradient direction to horizontal as we’re applying the new pixel value while moving through the columns (img[:, x] = color), and we’re specifying the gradient colors to start from black (0, 0, 0) to white (255, 255, 255):

We can change the gradient direction by moving through the rows instead of columns:

import cv2
import numpy as np

img = np.zeros((300, 300, 3), dtype=np.uint8)

for y in range(300):
    interp = y / (299)
    color = [
        int((0, 0, 0)[i] * (1 - interp) + (255, 255, 255)[i] * interp)
        for i in range(3)
    ]
    img[y, :] = color

cv2.imshow("image-window", img)
cv2.waitKey(0)
cv2.destroyAllWindows()

Or we can change the color gradient by changing the starting color:

import cv2
import numpy as np

img = np.zeros((300, 300, 3), dtype=np.uint8)

for y in range(300):
    interp = y / (299)
    color = [
        int((255, 0, 0)[i] * (1 - interp) + (255, 255, 255)[i] * interp)
        for i in range(3)
    ]
    img[y, :] = color

cv2.imshow("image-window", img)
cv2.waitKey(0)
cv2.destroyAllWindows()

Saving and Compressing Images: PNG, JPEG, and WebP

After creating an image and manipulating it as needed, we can save it as a file with a specific format and compression/quality level. OpenCV also provides a method called cv2.imwrite() that enables us to easily do this.

For example, let’s save the image where we added a text to a file with PNG format:

import cv2
import numpy as np

img = np.zeros((300, 300, 3), dtype=np.uint8)

cv2.putText(img, "Hello World!", (15, 165), cv2.FONT_HERSHEY_SIMPLEX, 1.5, (255, 0, 255), 5)

cv2.imwrite("img_txt.png", img)

The cv2.imwrite() takes as a parameter the file name that you want to store your image with, and the NumPy array of the image. The file name extension is used to determine the image format to use, so a .png extension tells OpenCV to save the image in PNG format.

The cv2.imwrite() has a third optional parameter which we can use to provide flags that specify properties for saving the image like compression level or quality. For example, we can set the compression level for the previous PNG image as follows:

import cv2
import numpy as np

img = np.zeros((300, 300, 3), dtype=np.uint8)

cv2.putText(img, "Hello World!", (15, 165), cv2.FONT_HERSHEY_SIMPLEX, 1.5, (255, 0, 255), 5)

cv2.imwrite("img_txt.png", img, [cv2.IMWRITE_PNG_COMPRESSION, 0])

Here, we used the compression level of 0 which means no image compression algorithms are used. This takes the shortest time to save the image file, but with the largest file size.

We can check the file size of the saved image as follows:

import cv2
import numpy as np
import os

img = np.zeros((300, 300, 3), dtype=np.uint8)

cv2.putText(img, "Hello World!", (15, 165), cv2.FONT_HERSHEY_SIMPLEX, 1.5, (255, 0, 255), 5)

cv2.imwrite("img_txt.png", img, [cv2.IMWRITE_PNG_COMPRESSION, 0])

file_size = os.path.getsize("img_txt.png")

print(file_size)

Now if we change the compression level to the highest value of 9, we should see the difference in image file size:

Similarly, we can save the image in a JPEG format by changing the extension to .jpg. The compression level of JPEG format is specified by setting an image quality value between 0-100, with 0 being the lowest quality/highest compression, and 100 is the highest quality/lowest compression:

import cv2
import numpy as np
import os

img = np.zeros((300, 300, 3), dtype=np.uint8)

cv2.putText(img, "Hello World!", (15, 165), cv2.FONT_HERSHEY_SIMPLEX, 1.5, (255, 0, 255), 5)

cv2.imwrite("img_txt.jpg", img, [cv2.IMWRITE_JPEG_QUALITY, 100])

file_size = os.path.getsize("img_txt.jpg")

print(file_size)

Another format that’s supported when saving the image is the WebP format. It also uses a quality parameter like the JPEG format for specifying the compression/quality level:

import cv2
import numpy as np
import os

img = np.zeros((300, 300, 3), dtype=np.uint8)

cv2.putText(img, "Hello World!", (15, 165), cv2.FONT_HERSHEY_SIMPLEX, 1.5, (255, 0, 255), 5)

cv2.imwrite("img_txt.webp", img, [cv2.IMWRITE_WEBP_QUALITY, 100])

file_size = os.path.getsize("img_txt.webp")

print(file_size)

Frequently Asked Questions

How to create an image in Python?

Python includes different libraries for image processing that help create images programmatically. To create an image, you initialize the data structure (like a NumPy array) with the image data with pixel values, then you use these libraries to read this data.

What Python library generates images?

OpenCV, Pillow, and Matplotlib are the most common Python libraries for creating/generating images.

What is a Python image library?

Python image libraries facilitate the process of creating, manipulating, storing, and displaying images. They abstract the complex low-level details and provide easy-to-use functions and objects to work with images.

QUICK TIPS
Jen Looper
Cloudinary Logo Jen Looper

In my experience, here are tips that can help you better create images in Python for drawing, compositing, exporting, and automation workflows:

  1. Pick one internal “canvas” standard early
    Decide upfront: RGB or RGBA, linear or sRGB, float or uint8. Convert all inputs to that standard at the boundary so every draw/composite step behaves predictably.
  2. Render in linear light when you care about quality
    Gradients, shadows, blur, and anti-aliased edges look cleaner if you composite in linear space, then convert back to sRGB for saving. It’s a subtle upgrade that makes generated graphics feel “pro.”
  3. Use premultiplied alpha for compositing-heavy pipelines
    If you’re layering lots of semi-transparent assets, premultiply once and keep it that way internally. You’ll avoid dark halos and edge fringing when resizing and blending.
  4. Avoid Python pixel loops—treat them as a code smell
    Anything “for x in width; for y in height” should become vectorized NumPy, OpenCV ops, or Pillow’s ImageDraw/filters. You’ll get huge speedups and lower CPU burn in services.
  5. Build a reusable layout system for text and spacing
    For dynamic banners/reports, define anchors (top-left, center, baseline), margins, and a simple box model. Most “generated image looks off” bugs are layout, not drawing.
  6. Control font rendering explicitly
    Ship the fonts you need with your app and load them directly (don’t rely on OS fonts). Also be consistent about hinting/antialias settings so outputs don’t change between dev and prod.
  7. Create “safe zones” and aspect-ratio variants automatically
    If images will be reused across platforms (social, thumbnails, cards), generate multiple aspect ratios from the same design spec and keep critical elements inside a safe zone.
  8. Prefer drawing at 2× or 4× then downsampling
    For crisp lines and text, render at higher resolution and downsample with a high-quality filter. This is a simple way to improve perceived sharpness without complex anti-alias tuning.
  9. Make outputs deterministic with seeded randomness
    If you add noise, textures, or randomized placements, seed your RNG and store the seed in metadata. It makes bug reports reproducible and allows “regenerate exact asset” workflows.
  10. Treat export settings as part of the design, not an afterthought
    For PNG: choose compression vs speed based on use (batch vs realtime). For JPEG/WebP: tune quality per content type (text-heavy graphics need higher quality than photos) to avoid ringing and mushy edges.
Last updated: Feb 8, 2026