diff --git a/basic_mer_tests.py b/basic_mer_tests.py new file mode 100644 index 0000000..4a8b3ec --- /dev/null +++ b/basic_mer_tests.py @@ -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() diff --git a/pds/imageextractor.py b/pds/imageextractor.py index 90a62e4..42af893 100644 --- a/pds/imageextractor.py +++ b/pds/imageextractor.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +# vim: set noexpandtab # encoding: utf-8 """ imageextractor.py @@ -8,7 +9,6 @@ Copyright (c) 2009 Ryan Matthew Balfanz. All rights reserved. """ - import hashlib import logging import os @@ -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 @@ -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() @@ -70,10 +72,10 @@ 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 @@ -81,11 +83,11 @@ def __init__(self, log=None, raisesChecksumError=True, raisesImageNotSupportedEr 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): @@ -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() @@ -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) @@ -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))) @@ -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'] @@ -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 . The image location is given by the value. - + This may raise a ValueError. """ imagePointer = self.labels['^IMAGE'].split() @@ -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"] @@ -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): @@ -289,4 +306,4 @@ def test_no_exceptions(self): if __name__ == '__main__': unittest.main() - \ No newline at end of file + diff --git a/tests.py b/tests.py index 2febfa1..5f8d7e0 100644 --- a/tests.py +++ b/tests.py @@ -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):