Skip to content

StringToImageDemo

Repository source: StringToImageDemo

Description

This example demonstrates the use of vtkFreeTypeTools to populate an image with multiple strings converted into images. The final image is created using vtkImageBlend to blend each string image into the final image. If a string image does not fit in the final image or overlaps with an image in final, that sting image is skipped.

This example differs from the C++ example in that vtkImageIterator cannot be used since it is designed for use for C++. Accordingly, we implement the Separating Axis Theorem to determine if the rectangles corresponding to the text overlap.

See: How to check intersection between 2 rotated rectangles?

Tfe final result differs slightly from the C++ example in that only 15 and not 19 images are rendered.

Other languages

See (Cxx)

Question

If you have a question about this example, please use the VTK Discourse Forum

Code

StringToImageDemo.py

#!/usr/bin/env python3

import numpy as np

# noinspection PyUnresolvedReferences
import vtkmodules.vtkInteractionStyle
# noinspection PyUnresolvedReferences
import vtkmodules.vtkRenderingFreeType
# noinspection PyUnresolvedReferences
import vtkmodules.vtkRenderingOpenGL2
from vtkmodules.vtkCommonColor import vtkNamedColors
from vtkmodules.vtkCommonCore import vtkMinimalStandardRandomSequence
from vtkmodules.vtkCommonDataModel import vtkImageData
from vtkmodules.vtkImagingCore import vtkImageBlend
from vtkmodules.vtkImagingSources import vtkImageCanvasSource2D
from vtkmodules.vtkInteractionImage import vtkImageViewer2
from vtkmodules.vtkRenderingCore import (
    vtkRenderWindowInteractor, vtkTextProperty
)
from vtkmodules.vtkRenderingFreeType import vtkFreeTypeTools


def get_program_parameters():
    import argparse
    description = 'Extract a surface from vtkPolyData points.'
    epilogue = '''
    '''
    parser = argparse.ArgumentParser(description=description, epilog=epilogue,
                                     formatter_class=argparse.RawTextHelpFormatter)
    parser.add_argument('text', nargs='?', default=None,
                        help='The optional text (enclosed in quotes) to convert to an image.')
    args = parser.parse_args()

    return args.text


def main():
    text = get_program_parameters()
    if text is None:
        text = 'Old Guys Rule'

    # Create a blank, black image
    drawing = vtkImageCanvasSource2D(number_of_scalar_components=3, extent=(0, 640, 0, 480, 0, 0), draw_color=(0, 0, 0))
    drawing.SetScalarTypeToUnsignedChar()
    drawing.FillBox(0, 629, 0, 479)

    # Create an image that will hold the final image.
    final = vtkImageBlend()
    drawing >> final
    final.opacity = 0, 1.0
    final.update()

    # Create an image of the string.
    dpi = 150
    free_type = vtkFreeTypeTools.GetInstance()
    free_type.ScaleToPowerTwoOff()

    # Set up a property for the strings containing fixed parameters.
    colors = vtkNamedColors()
    text_property = vtkTextProperty(color=colors.GetColor3d('Tomato'))
    text_property.SetVerticalJustificationToCentered()
    text_property.SetJustificationToCentered()

    random_sequence = vtkMinimalStandardRandomSequence()
    random_sequence.SetSeed(8775070)

    # For each string, create an image and see if it overlaps
    # with other images, if so, skip it.
    tried = 600
    accepted = 0
    count = 0
    valid_extents = list()
    overlap = 0
    for i in range(0, tried):
        font_size = random_sequence.GetRangeValue(6, 42)
        random_sequence.Next()
        text_property.orientation = random_sequence.GetRangeValue(-90, 90)
        random_sequence.Next()
        text_property.font_size = int(font_size)

        text_image = vtkImageData()
        free_type.RenderString(text_property, text, dpi, text_image)

        # Set the extent of the text image.
        bb = [0] * 4
        free_type.GetBoundingBox(text_property, text, dpi, bb)
        offset_x = int(random_sequence.GetRangeValue(0, 640))
        random_sequence.Next()
        offset_y = int(random_sequence.GetRangeValue(0, 480))
        random_sequence.Next()
        # Make sure the text image will fit on the final image.
        good = True
        if offset_x + bb[1] - bb[0] < 639 and offset_y + bb[3] - bb[2] < 479:
            count += 1
            extent = (offset_x, offset_x + bb[1] - bb[0], offset_y, offset_y + bb[3] - bb[2], 0, 0)
            text_image.SetExtent(extent)
            image = vtkImageData()
            final.update()
            # Convert the valid extent to a series of points ordered in a
            # counter-clockwise  direction. This corresponds to the rectangle.
            a = np.array([[extent[0], extent[2]], [extent[1], extent[2]],
                          [extent[1], extent[2]], [extent[1], extent[3]],
                          [extent[1], extent[3]], [extent[0], extent[3]],
                          [extent[0], extent[3]], [extent[0], extent[2]]])

            if i > 1:
                for ext in valid_extents:
                    # Convert the valid extent to a series of points ordered in a
                    # counter-clockwise  direction. This corresponds to the rectangle.
                    b = np.array([[ext[0], ext[2]], [ext[1], ext[2]],
                                  [ext[1], ext[2]], [ext[1], ext[3]],
                                  [ext[1], ext[3]], [ext[0], ext[3]],
                                  [ext[0], ext[3]], [ext[0], ext[2]]])
                    # Do the rectangles intersect?
                    res = do_polygons_intersect(a, b)
                    good = not res and good
                    if res:
                        overlap += 1
                        break
                if good:
                    accepted += 1
                    image.DeepCopy(text_image)
                    final.AddInputData(image)
                    final.opacity = (i + 1, 1.0)  # text: 100% opaque
                    final.Update()
                    valid_extents.append(extent)
            else:
                # The first extent is always valid.
                accepted += 1
                image.DeepCopy(text_image)
                final.AddInputData(image)
                final.opacity = (i + 1, 1.0)  # text: 100% opaque
                final.Update()
                valid_extents.append(extent)

    print(f'Tried {tried}, but only accepted {accepted}.')

    # Display the result.
    interactor = vtkRenderWindowInteractor()

    image_viewer = vtkImageViewer2(size=(640, 512), input_data=final.output)
    image_viewer.SetupInteractor(interactor)
    image_viewer.GetRenderer().background = colors.GetColor3d('Wheat')
    image_viewer.GetRenderer().ResetCamera()
    image_viewer.GetRenderWindow().window_name = 'StringToImageDemo'

    camera = image_viewer.GetRenderer().active_camera
    camera.ParallelProjectionOn()
    camera.SetParallelScale(640 * 0.4)
    image_viewer.GetRenderWindow().Render()
    interactor.Initialize()
    interactor.Start()


def do_polygons_intersect(a, b):
    """
    A function to determine whether there is an intersection
     between the two polygons described by the lists of vertices.

    Uses the Separating Axis Theorem.

    See: [How to check intersection between 2 rotated rectangles?](https://stackoverflow.com/questions/10962379/how-to-check-intersection-between-2-rotated-rectangles)

    :param a: The ndarray of connected points [[x_1, y_1], [x_2, y_2],...] that form a closed polygon
    :param b: The ndarray of connected points [[x_1, y_1], [x_2, y_2],...] that form a closed polygon
    :return: True if there is any intersection between the 2 polygons, False otherwise
    """

    polygons = [a, b]

    for i in range(len(polygons)):

        # For each polygon, look at each edge of the polygon,
        # and determine if it separates the two shapes.
        polygon = polygons[i]
        for i1 in range(len(polygon)):

            # Grab 2 vertices to create an edge.
            i2 = (i1 + 1) % len(polygon)
            p1 = polygon[i1]
            p2 = polygon[i2]

            # Find the line perpendicular to this edge.
            normal = {'x': p2[1] - p1[1], 'y': p1[0] - p2[0]}

            min_a, max_a = None, None
            # For each vertex in the first shape, project it onto the line
            # perpendicular to the edge and keep track of the min and max
            # of these values.
            for j in range(len(a)):
                projected = normal['x'] * a[j][0] + normal['y'] * a[j][1]
                if (min_a is None) or (projected < min_a):
                    min_a = projected

                if (max_a is None) or (projected > max_a):
                    max_a = projected

            # For each vertex in the second shape, project it onto the line
            # perpendicular to the edge and keep track of the min and max
            # of these values.
            min_b, max_b = None, None
            for j in range(len(b)):
                projected = normal['x'] * b[j][0] + normal['y'] * b[j][1]
                if (min_b is None) or (projected < min_b):
                    min_b = projected
                if (max_b is None) or (projected > max_b):
                    max_b = projected

            # If there is no overlap between the projections, the edge we are
            # looking at separates the two polygons, and we know there is no overlap.
            if (max_a < min_b) or (max_b < min_a):
                # print('polygons don't intersect!')
                return False

    return True


if __name__ == '__main__':
    main()