Skip to content
This repository has been archived by the owner on Jun 30, 2020. It is now read-only.

Commit

Permalink
use PixelArray instead of plain arrays, should make stuff faster
Browse files Browse the repository at this point in the history
Updated BMP/JPEG decoders to support the new pixel arrays
  • Loading branch information
ojii committed Jul 6, 2012
1 parent f92347d commit e6f63df
Show file tree
Hide file tree
Showing 9 changed files with 346 additions and 122 deletions.
65 changes: 22 additions & 43 deletions pymaging/image.py
Expand Up @@ -34,14 +34,14 @@


class Image(object):
def __init__(self, width, height, pixels, mode, palette=None):
self.width = width
self.height = height
self.pixels = pixels
def __init__(self, pixelarray, mode, palette=None):
self.mode = mode
self.palette = palette
self.reverse_palette = None
self.pixelsize = 1 if self.palette else self.mode.length
self.pixels = pixelarray
self.width = self.pixels.width
self.height = self.pixels.height
self.pixelsize = self.pixels.pixelsize

#==========================================================================
# Constructors
Expand Down Expand Up @@ -103,8 +103,6 @@ def resize(self, width, height, resample_algorithm=nearest, resize_canvas=True):
self, width, height, resize_canvas=resize_canvas
)
return Image(
width,
height,
pixels,
self.mode,
self.palette,
Expand All @@ -121,8 +119,6 @@ def affine(self, transform, resample_algorithm=nearest, resize_canvas=True):
resize_canvas=resize_canvas,
)
return Image(
len(pixels[0]) / self.pixelsize if pixels else 0,
len(pixels),
pixels,
self.mode,
self.palette,
Expand All @@ -148,27 +144,20 @@ def rotate(self, degrees, clockwise=False, resample_algorithm=nearest, resize_ca

pixels = resample_algorithm.affine(self, transform, resize_canvas=resize_canvas)
return Image(
(len(pixels[0]) / self.pixelsize) if pixels else 0,
len(pixels),
pixels,
self.mode,
self.palette,
)

def get_pixel(self, x, y):
line = self.pixels[y]
if self.pixelsize == 1:
pixel = line[x]
if self.palette:
return self.palette[pixel]
else:
return [pixel]
try:
raw_pixel = self.pixels.get(x, y)
except IndexError:
raise IndexError("Pixel (%d, %d) not in image" % (x, y))
if self.pixelsize == 1 and self.palette:
return self.palette[raw_pixel[0]]
else:
start = x * self.pixelsize
pixel = line[start:start + self.pixelsize]
if not pixel:
raise IndexError("Pixel (%d, %d) not in image" % (x, y))
return pixel
return raw_pixel

def get_color(self, x, y):
return Color.from_pixel(self.get_pixel(x, y))
Expand All @@ -181,21 +170,16 @@ def set_color(self, x, y, color):
if color not in self.reverse_palette:
raise InvalidColor(str(color))
index = self.reverse_palette[color]
self.pixels[y][x] = index
self.pixels.set(x, y, [index])
else:
start = x * self.pixelsize
end = start + self.pixelsize
pixel = color.to_pixel(self.pixelsize)
self.pixels[y][start:end] = array.array('B', pixel)
self.pixels.set(x, y, color.to_pixel(self.pixelsize))

def flip_top_bottom(self):
"""
Vertically flips the pixels of source into target
"""
return Image(
self.width,
self.height,
[array.array(line.typecode, line) for line in reversed(self.pixels)],
self.pixels.copy_flipped_top_bottom(),
self.mode,
self.palette,
)
Expand All @@ -204,25 +188,20 @@ def flip_left_right(self):
"""
Horizontally flips the pixels of source into target
"""
if self.pixelsize == 1:
flipper = reversed
else:
flipper = Fliprow(self.width * self.pixelsize, self.pixelsize).flip
return Image(
self.width,
self.height,
[flipper(line) for line in self.pixels],
self.pixels.copy_flipped_left_right(),
self.mode,
self.palette,
)

def crop(self, width, height, padding_top, padding_left):
linestart = padding_left * self.pixelsize
lineend = linestart + (width * self.pixelsize)
new_pixels = self.pixels.copy()
new_pixels.remove_lines(0, padding_top)
new_pixels.remove_lines(height, new_pixels.height - height)
new_pixels.remove_columns(0, padding_left)
new_pixels.remove_columns(width, new_pixels.width - width)
return Image(
width,
height,
[line[linestart:lineend] for line in self.pixels[padding_top:padding_top + height]],
new_pixels,
self.mode,
self.palette,
)
Expand Down
2 changes: 0 additions & 2 deletions pymaging/incubator/formats/__init__.py
Expand Up @@ -25,10 +25,8 @@

from pymaging.formats import registry
from pymaging.incubator.formats.jpg import JPG
from pymaging.incubator.formats.png import PNG

INCUBATOR_FORMATS = [
PNG,
JPG,
]

Expand Down
73 changes: 48 additions & 25 deletions pymaging/incubator/formats/bmp/codec.py
Expand Up @@ -27,6 +27,7 @@
from pymaging.image import Image
import array
import struct
from pymaging.pixelarray import get_pixel_array


def BITMAPINFOHEADER(decoder):
Expand All @@ -36,9 +37,12 @@ def BITMAPINFOHEADER(decoder):
assert nplanes == 1, nplanes
if decoder.bits_per_pixel == 32:
decoder.read_row = decoder.read_row_32bit
decoder.pixelsize = 3
elif decoder.bits_per_pixel == 24:
decoder.read_row = decoder.read_row_24bit
decoder.pixelsize = 3
elif decoder.bits_per_pixel == 1:
decoder.pixelsize = 1
decoder.read_row = decoder.read_row_1bit

def BITMAPV2INFOHEADER(decoder):
Expand Down Expand Up @@ -75,7 +79,20 @@ def read_header(self):
pre_header = self.fileobj.tell()
headersize = struct.unpack('<i', self.fileobj.read(4))[0]
palette_start = pre_header + headersize
# this will set the following attributes:
# width
# height
# bits_per_pixel
# compression_method
# bmp_bytesz
# hres
# vres
# ncolors
# nimpcolors
# read_row
# pixelsize
HEADERS[headersize](self)
self.pixelwidth = self.width * self.pixelsize
self.row_size = ((self.bits_per_pixel * self.width) // 32) * 4
# there might be header stuff that wasn't read, so skip ahead to the
# start of the color palette
Expand All @@ -87,42 +104,48 @@ def read_header(self):
# set palette to None instead of empty list when there's no palette
self.palette = palette or None

def read_row_32bit(self):
row = array.array('B')
for _ in range(self.width):
def read_row_32bit(self, pixel_array, row_num):
row_start = row_num * self.pixelwidth
for x in range(self.width):
# not sure what the first thing is used for
_, b, g, r = struct.unpack('<BBBB', self.fileobj.read(4))
row.extend([r, g, b]) # bgr->rgb
return row

def read_row_24bit(self):
row = array.array('B')
rowlength = self.width * 3
row.fromfile(self.fileobj, rowlength)
self.fileobj.read(rowlength % 4) # padding
return row

def read_row_1bit(self):
start = row_start + (x * self.pixelsize)
pixel_array.data[start] = r
pixel_array.data[start + 1] = g
pixel_array.data[start + 2] = b

def read_row_24bit(self, pixel_array, row_num):
row = array.array('B')
row.fromfile(self.fileobj, self.pixelwidth)
start = row_num * self.pixelwidth
end = start + self.pixelwidth
pixel_array.data[start:end] = row
self.fileobj.read(self.pixelwidth % 4) # padding

def read_row_1bit(self, pixel_array, row_num):
padding = 32 - (self.width % 32)
rowlength = (self.width + padding) // 8
bits = []
for b in struct.unpack('%sB' % rowlength, self.fileobj.read(rowlength)):
row_length = (self.width + padding) // 8
start = row_num * self.pixelwidth
item = 0
for b in struct.unpack('%sB' % row_length, self.fileobj.read(row_length)):
for _ in range(8):
a, b = divmod(b, 128)
bits.append(a)
pixel_array.data[start + item] = a
item += 1
if item >= self.width:
return
b <<= 1
row.fromlist(bits[:self.width])
return row


def get_image(self):
# go to the start of the pixel array
self.fileobj.seek(self.offset)
# since bmps are stored upside down, initialize a pixel list
pixels = [None for _ in range(self.height)]
initial = array.array('B', [0] * self.width * self.height * self.pixelsize)
pixel_array = get_pixel_array(initial, self.width, self.height, self.pixelsize)
# iterate BACKWARDS over the line indices so we don't have to reverse
# later. this is why we intialize pixels above.
for index in range(self.height - 1, -1, -1):
pixels[index] = self.read_row()
for row_num in range(self.height - 1, -1, -1):
self.read_row(pixel_array, row_num)
# TODO: Not necessarily RGB
return Image(self.width, self.height, pixels, RGB, palette=self.palette)

return Image(pixel_array, RGB, palette=self.palette)
23 changes: 15 additions & 8 deletions pymaging/incubator/formats/jpg/__init__.py
Expand Up @@ -29,6 +29,9 @@
from pymaging.image import Image
from pymaging.incubator.formats.jpg.raw import TonyJpegDecoder
import array
from pymaging.pixelarray import get_pixel_array

PIXELSIZE = 3

def decode(fileobj):
decoder = TonyJpegDecoder()
Expand All @@ -38,14 +41,18 @@ def decode(fileobj):
except:
fileobj.seek(0)
return None
# bmpout is in bgr format, bottom to top. it has padding stuff.
pixels = []
for _ in range(0, decoder.Height):
#TODO: flip bgr to rgb
pixels.append(array.array('B', bmpout[:3 * decoder.Width]))
del bmpout[:3 * decoder.Width]
del bmpout[:2] # kill padding
return Image(decoder.Width, decoder.Height, list(reversed(pixels)), RGB)
pixels = array.array('B')
row_width = decoder.Width * PIXELSIZE
# rows are bottom to top
for reversed_row_num in range(decoder.Height - 1, -1, -1):
start = reversed_row_num * (row_width + 2)
end = start + row_width
pixels.extend(bmpout[start:end])
#pixels.extend(bmpout[:3 * decoder.Width])
#del bmpout[:3 * decoder.Width]
#del bmpout[:2] # kill padding
pixel_array = get_pixel_array(pixels, decoder.Width, decoder.Height, PIXELSIZE)
return Image(pixel_array, RGB)

def encode(image, fileobj):
raise FormatNotSupported('jpeg')
Expand Down
14 changes: 6 additions & 8 deletions pymaging/incubator/formats/jpg/tests.py
Expand Up @@ -25,21 +25,19 @@
from pymaging import Image
from pymaging.colors import Color
from pymaging.incubator.formats import register
from pymaging.tests.test_basic import PymagingBaseTestCase
from pymaging.utils import get_test_file
from pymaging.webcolors import Black, White
import unittest

ALMOST_BLACK = Color(8, 8,8 , 255)

class JPGTests(unittest.TestCase):
class JPGTests(PymagingBaseTestCase):
def setUp(self):
register()

def test_decode(self):
img = Image.open_from_path(get_test_file(__file__, 'black-white-100.jpg'))
self.assertEqual(img.get_color(0, 0), Black)
# TODO: Is this correct? Is this just JPEG being JPEG or is the decoder
# buggy? 1/1 SHOULD be BLACK but it's 8 8 8.
self.assertEqual(img.get_color(1, 1), ALMOST_BLACK)
self.assertEqual(img.get_color(0, 1), White)
self.assertEqual(img.get_color(1, 0), White)
self.assertImage(img, [
[Black, White],
[White, ALMOST_BLACK] # no clue why this is "almost" black
], False)

0 comments on commit e6f63df

Please sign in to comment.