Resize rasterized text and make it look non-pixellated

11

1

This is a screenshot of some text typed in a text editor:

16px-high text

This is the same text at a larger size.

96px-high text

Notice how visible the aliasing is on letters with prominent diagonal strokes like x and z. This issue is a major reason why raster fonts have lost popularity to “scalable” formats like TrueType.

But maybe this isn't an inherent problem with raster fonts, just with the way scaling of them is typically implemented. Here's an alternative rendering using simple bilinear interpolation combined with thresholding.

96px-high text rendered with bilinear interpolation

This is smoother, but not ideal. Diagonal strokes are still bumpy, and curved letters like c and o are still polygons. This is especially noticeable at large sizes.

So is there a better way?

The task

Write a program that takes three command-line arguments.

resize INPUT_FILE OUTPUT_FILE SCALE_FACTOR

where

  • INPUT_FILE is the name of the input file, which is assumed to be an image file containing black text on a white background. You may use any mainstream raster image format (PNG, BMP, etc.) that's convenient.
  • OUTPUT_FILE is the name of the output file. It can be either a raster or vector image format. You may introduce color if you're doing some ClearType-like subpixel rendering.
  • SCALE_FACTOR is a positive floating-point value that indicates how much the image may be resized. Given an x × y px input file and scaling factor s, the output will have a size of sx × sy px (rounded to integers).

You may use a third-pary open-source image processing library.

In addition to your code, include sample outputs of your program at scale factors of 1.333, 1.5, 2, 3, and 4 using my first image as input. You may also try it with other fonts, including proportionally-spaced ones.

Scoring

This is a popularity contest. The entry with the greatest number of upvotes minus downvotes wins. In case of an exact tie, the earlier entry wins.

Edit: Deadline extended due to lack of entries. TBA.

Voters are encouraged to judge based primarily on how good the output images look, and secondarily on the simplicity/elegance of the algorithm.

dan04

Posted 2015-04-30T03:20:55.673

Reputation: 6 319

Is SCALE_FACTOR always > 1? – kennytm – 2015-04-30T19:17:14.677

@kennytm: Yes. Have edited to explicitly list the scale factors. – dan04 – 2015-05-01T01:47:32.137

Can we assume there is only one line of text in the image? – GiantTree – 2015-05-01T20:40:19.117

@GiantTree: Yes. You can support multi-line text if you want, but this isn't required. – dan04 – 2015-05-02T00:35:39.717

Answers

4

Ruby, with RMagick

The algorithm is very simple—find patterns of pixels that look like this:

    ####
    ####
    ####
    ####
########
########
########
########

and add triangles to make them look like this:

    ####
   #####
  ######
 #######
########
########
########
########

Code:

#!/usr/bin/ruby

require 'rmagick'
require 'rvg/rvg'
include Magick

img = Image.read(ARGV[0] || 'img.png').first
pixels = []
img.each_pixel{|px, x, y|
    if px.red == 0 && px.green == 0 && px.blue == 0
        pixels.push [x, y]
    end
}

scale = ARGV[2].to_f || 5.0
rvg = RVG.new((img.columns * scale).to_i, (img.rows * scale).to_i)
    .viewbox(0, 0, img.columns, img.rows) {|cnv|
    # draw all regular pixels
    pixels.each do |p|
        cnv.rect(1, 1, p[0], p[1])
    end
    # now collect all 2x2 rectangles of pixels
    getpx = ->x, y { !!pixels.find{|p| p[0] == x && p[1] == y } }
    rects = [*0..img.columns].product([*0..img.rows]).map{|x, y|
        [[x, y], [
            [getpx[x, y  ], getpx[x+1, y  ]],
            [getpx[x, y+1], getpx[x+1, y+1]]
        ]]
    }
    # WARNING: ugly code repetition ahead
    # (TODO: ... fix that)
    # find this pattern:
    # ?X
    # XO
    # where X = black pixel, O = white pixel, ? = anything
    rects.select{|r| r[1][0][1] && r[1][1][0] && !r[1][2][1] }
        .each do |r|
            x, y = r[0]
            cnv.polygon x+1,y+1, x+2,y+1, x+1,y+2
        end
    # OX
    # X?
    rects.select{|r| r[1][0][1] && r[1][3][0] && !r[1][0][0] }
        .each do |r|
            x, y = r[0]
            cnv.polygon x+1,y+1, x+0,y+1, x+1,y+0
        end
    # X?
    # OX
    rects.select{|r| r[1][0][0] && r[1][4][1] && !r[1][5][0] }
        .each do |r|
            x, y = r[0]
            cnv.polygon x+1,y+1, x+0,y+1, x+1,y+2
        end
    # XO
    # ?X
    rects.select{|r| r[1][0][0] && r[1][6][1] && !r[1][0][1] }
        .each do |r|
            x, y = r[0]
            cnv.polygon x+1,y+1, x+2,y+1, x+1,y+0
        end
}
rvg.draw.write(ARGV[1] || 'out.png')

Outputs (click any to view image by itself):

1.333

1.333

1.5

1.5

2

2

3

3

4

4

Doorknob

Posted 2015-04-30T03:20:55.673

Reputation: 68 138