Skip to content

Commit

Permalink
Merge pull request #13 from godber/master
Browse files Browse the repository at this point in the history
Handling standard 16 bit images (Issue #11)
  • Loading branch information
RyanBalfanz committed Jan 26, 2013
2 parents db560b1 + 0794e04 commit c0960a0
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 47 deletions.
63 changes: 63 additions & 0 deletions basic_mer_tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#!/usr/bin/env python2.6
# vim: set noexpandtab
# encoding: utf-8
"""
basic_mer_tests.py
Created by Austin Godber - 2013-01-12.
Copyright (c) 2013 Austin Godber. All rights reserved.
"""

import os
import unittest

from pds.core.common import open_pds
from pds.core.parser import Parser
from pds.imageextractor import ImageExtractor


class untitledTests(unittest.TestCase):
def setUp(self):
self.parser = Parser()
self.imageExtractor = ImageExtractor()
self.testDataPath = "../test_data/MER"
self.testImages = ["1F345867992EFFB0J3P1212L0M1.IMG",
"1N345854840EFFB0IEP1994L0M1.IMG",
"1P345688456EFFB0EJP2363L2C1.IMG"]
self.testImagePath = self.testDataPath + "/" + self.testImages[0]

def test_existence(self):
assert os.path.exists(self.testImagePath)

def test_open_pds(self):
"""Opening a single image without an exception"""
p = self.parser
try:
f = open_pds(self.testImagePath)
p.parse(f)
except:
raise

def test_get_labels(self):
"""Parsing labels returns something other than None"""
p = self.parser
labels = p.parse(open_pds(self.testImagePath))
self.assertNotEqual(None, labels)

def test_get_image(self):
"""Verify that the extracted image dimensions match those in the label"""
ie = self.imageExtractor
img, labels = ie.extract(open_pds(self.testImagePath))
self.assertNotEqual(None, img)
imageSize = map(int, (labels["IMAGE"]["LINE_SAMPLES"], labels["IMAGE"]["LINES"]))
self.assertEqual(tuple(imageSize), img.size)

def test_write_images(self):
"""docstring for test_write_image"""
ie = self.imageExtractor
for image in self.testImages:
img, labels = ie.extract(open_pds(self.testDataPath + '/' + image))
img.save(image + '.jpg')

if __name__ == '__main__':
unittest.main()
109 changes: 63 additions & 46 deletions pds/imageextractor.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#!/usr/bin/env python
# vim: set noexpandtab
# encoding: utf-8
"""
imageextractor.py
Expand All @@ -8,7 +9,6 @@
Copyright (c) 2009 Ryan Matthew Balfanz. All rights reserved.
"""


import hashlib
import logging
import os
Expand All @@ -18,8 +18,10 @@
try:
import Image
except ImportError:
from PIL import Image

from PIL import Image

import ImageMath

from core.common import open_pds
from core.parser import Parser
from core.extractorbase import ExtractorBase, ExtractorError
Expand All @@ -30,37 +32,37 @@ class ImageExtractorError(ExtractorError):

def __init__(self, *args, **kwargs):
super(ExtractorError, self).__init__(*args, **kwargs)


class ImageNotSupportedError(object):
"""docstring for ImageNotSupportedError"""
def __init__(self):
super(ImageNotSupportedError, self).__init__()


class ChecksumError(ImageExtractorError):
"""Error raised when verification of a secure hash fails."""

def __init__(self, *args, **kwargs):
super(ChecksumError, self).__init__(*args, **kwargs)


class ImageExtractor(ExtractorBase):
"""Extract an image embedded within a PDS file.
Returned images are instances of the Python Imaging Library Image class.
As such, this module depends on PIL.
An attached image may be extracted from by
An attached image may be extracted from by
determining its location within the file and identifying its size.
Not all PDS images are supported at this time.
Currently this module only supports FIXED_LENGTH as the RECORD_TYPE,
8 as the SAMPLE_BITS and either UNSIGNED_INTEGER or MSB_UNSIGNED_INTEGER as the SAMPLE_TYPE.
Attempts to extract an image that is not supported will result in None being returned.
Simple Example Usage
>>> import Image
>>> from imageextractor import ImageExtractor
>>> ie = ImageExtractor()
Expand All @@ -70,22 +72,22 @@ class ImageExtractor(ExtractorBase):
>>> else:
>>> print "The image was not supported."
"""

def __init__(self, log=None, raisesChecksumError=True, raisesImageNotSupportedError=True):
super(ImageExtractor, self).__init__()

self.log = log
self.raisesChecksumError = raisesChecksumError
self.raisesImageNotSupportedError = raisesImageNotSupportedError
if log:
self._init_logging()
# self.fh = None
# self.imageDimensions = None

# self.PILSettings = {}
# self.PILSettings["mode"] = "L"
# self.PILSettings["decoder"] = "raw"

# self.verifySecureHash = True

def _init_logging(self):
Expand All @@ -110,10 +112,10 @@ def _init_logging(self):
self.log.addHandler(stderr_hand)

self.log.debug('Initializing logger')

def extract(self, source):
"""Extract an image from *source*.
If the image is supported an instance of PIL's Image is returned, otherwise None.
"""
p = Parser()
Expand All @@ -125,11 +127,17 @@ def extract(self, source):
if self.log: self.log.debug("Image in '%s' is supported" % (source))
dim = self._get_image_dimensions()
loc = self._get_image_location()
imageSampleBits = int(self.labels['IMAGE']['SAMPLE_BITS'])
imageSampleType = self.labels['IMAGE']['SAMPLE_TYPE']
md5Checksum = self._get_image_checksum()
if self.log: self.log.debug("Image dimensions should be %s" % (str(dim)))
if self.log: self.log.debug("Seeking to image data at %d" % (loc))
f.seek(loc)
readSize = dim[0] * dim[1]
if imageSampleBits == 8:
readSize = dim[0] * dim[1]
elif imageSampleBits == 16:
readSize = dim[0] * dim[1] * 2
print readSize
if self.log: self.log.debug("Seek successful, reading data (%s)" % (readSize))
# rawImageData = f.readline()
# f.seek(-int(self.labels["RECORD_BYTES"]), os.SEEK_CUR)
Expand All @@ -148,7 +156,12 @@ def extract(self, source):
# The frombuffer defaults may change in a future release;
# for portability, change the call to read:
# frombuffer(mode, size, data, 'raw', mode, 0, 1).
img = Image.frombuffer('L', dim, rawImageData, 'raw', 'L', 0, 1)
if (imageSampleBits == 16) and imageSampleType == ('MSB_INTEGER'):
#img = Image.frombuffer('I', dim, rawImageData, 'raw', 'I;16BS', 0, 1)
img = Image.frombuffer('F', dim, rawImageData, 'raw', 'F;16B', 0, 1)
img = ImageMath.eval("convert(a/16.0, 'L')", a=img)
else:
img = Image.frombuffer('L', dim, rawImageData, 'raw', 'L', 0, 1)
if self.log:
self.log.debug("Image result: %s" % (str(img)))
self.log.debug("Image info: %s" % (str(img.info)))
Expand All @@ -158,22 +171,26 @@ def extract(self, source):
if self.log: self.log.error("Image is not supported '%s'" % (source))
img = None
f.close()

return img, self.labels

def _check_image_is_supported(self):
"""Check that the image is supported."""
SUPPORTED = {}
SUPPORTED['RECORD_TYPE'] = 'FIXED_LENGTH',
SUPPORTED['SAMPLE_BITS'] = 8,
SUPPORTED['SAMPLE_TYPE'] = 'UNSIGNED_INTEGER', 'MSB_UNSIGNED_INTEGER', 'LSB_INTEGER'

SUPPORTED['SAMPLE_BITS'] = 8, 16
SUPPORTED['SAMPLE_TYPE'] = ( 'UNSIGNED_INTEGER',
'MSB_UNSIGNED_INTEGER',
'LSB_INTEGER',
'MSB_INTEGER'
)

imageIsSupported = True

if not self.labels.has_key('IMAGE'):
if self.log: self.log.warn("No image data found")
imageIsSupported = False

recordType = self.labels['RECORD_TYPE']
imageSampleBits = int(self.labels['IMAGE']['SAMPLE_BITS'])
imageSampleType = self.labels['IMAGE']['SAMPLE_TYPE']
Expand All @@ -193,30 +210,30 @@ def _check_image_is_supported(self):
if self.raisesImageNotSupportedError:
raise ImageNotSupportedError(errorMessage)
imageIsSupported = False

return imageIsSupported

def _get_image_dimensions(self):
"""Return the dimensions of the image as (width, height).
The image size is expected to be defined by the labels LINES_SAMPLES and LINES as width and height, respectively.
"""
imageWidth = int(self.labels['IMAGE']['LINE_SAMPLES'])
imageHeight = int(self.labels['IMAGE']['LINES'])
return imageWidth, imageHeight

def _get_image_location(self):
"""Return the seek-able position of the image.
The seek byte is defined by the ^IMAGE pointer.
It may be a single value or a value accompanied by its units.
If only a single value is given the image location is given by
If only a single value is given the image location is given by
the product of the (value - 1) and RECORD_BYTES.
If the units are given along with the value, they must be <BYTES>.
The image location is given by the value.
This may raise a ValueError.
"""
imagePointer = self.labels['^IMAGE'].split()
Expand All @@ -234,15 +251,15 @@ def _get_image_location(self):
errorMessage = ("^IMAGE contains extra information") % (imageSampleType)
raise ValueError(errorMessage)
return imageLocation

def _get_image_checksum(self):
"""Return the md5 checksum of the image.
The checksum is retrieved from self.labels['IMAGE']['MD5_CHECKSUM'].
This may raise a KeyError.
"""
ignoreKeyError = True

md5Checksum = None
try:
md5Checksum = self.labels['IMAGE']["MD5_CHECKSUM"]
Expand All @@ -254,10 +271,10 @@ def _get_image_checksum(self):
else:
if self.log: self.log.debug("Found md5 checksum")
md5Checksum = md5Checksum[1:-1]

return md5Checksum


class ImageExtractorTests(unittest.TestCase):
"""Unit tests for class ImageExtractor"""
def setUp(self):
Expand Down Expand Up @@ -289,4 +306,4 @@ def test_no_exceptions(self):

if __name__ == '__main__':
unittest.main()

2 changes: 1 addition & 1 deletion tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class untitledTests(unittest.TestCase):
def setUp(self):
self.parser = Parser()
self.imageExtractor = ImageExtractor()
self.testImagePath = "./I18584006BTR.IMG"
self.testImagePath = "../test_data/I18584006BTR.IMG"
assert os.path.exists(self.testImagePath)

def test_open_pds(self):
Expand Down

0 comments on commit c0960a0

Please sign in to comment.