Source code for rawkit.raw

""":mod:`rawkit.raw` --- High-level raw file API
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
"""

import ctypes
import os
import random
import string
import tempfile
import warnings

from collections import namedtuple
from libraw.bindings import LibRaw
from libraw.errors import raise_if_error

from rawkit.errors import InvalidFileType
from rawkit.errors import NoFileSpecified
from rawkit.metadata import Metadata
from rawkit.options import Options
from rawkit.orientation import get_orientation


output_file_types = namedtuple(
    'OutputFileType', ['ppm', 'tiff']
)('ppm', 'tiff')

"""
Constants for setting the output filetype.

  - ``ppm`` --- PGM data file.
  - ``tiff`` --- TIFF file.
"""


[docs]class Raw(object): """ Represents a raw file (of any format) and exposes development options to the user. For example, the basic workflow (open a file, process the file, save the file) looks like this:: from rawkit.raw import Raw from rawkit.options import WhiteBalance with Raw(filename='some/raw/image.CR2') as raw: raw.options.white_balance = WhiteBalance(camera=False, auto=True) raw.save(filename='some/destination/image.ppm') Args: filename (str): The name of a raw file to load. Returns: Raw: A raw object. Raises: rawkit.errors.NoFileSpecified: If `filename` is ``None``. libraw.errors.FileUnsupported: If the specified file is not a supported raw type. libraw.errors.InsufficientMemory: If we run out of memory while loading the raw file. IOError: If the file does not exist, or cannot be opened (eg. incorrect permissions). """ def __init__(self, filename=None): """Initializes a new Raw object.""" if filename is None: raise NoFileSpecified() self.libraw = LibRaw() self.data = self.libraw.libraw_init(0) self.libraw.libraw_open_file(self.data, filename.encode('ascii')) self.options = Options() self.image_unpacked = False self.thumb_unpacked = False def __enter__(self): """Return a Raw object for use in context managers.""" return self def __exit__(self, exc_type, exc_value, traceback): """Clean up after ourselves when leaving the context manager.""" self.close()
[docs] def close(self): """Free the underlying raw representation.""" self.libraw.libraw_close(self.data)
[docs] def unpack(self): """Unpack the raw data.""" if not self.image_unpacked: self.libraw.libraw_unpack(self.data) self.image_unpacked = True
[docs] def unpack_thumb(self): """ Unpack the thumbnail data. Raises: libraw.errors.NoThumbnail: If the raw file does not contain a thumbnail. libraw.errors.UnsupportedThumbnail: If the thumbnail format is unsupported. """ if not self.thumb_unpacked: self.libraw.libraw_unpack_thumb(self.data) self.thumb_unpacked = True
[docs] def process(self): """ Process the raw data based on ``self.options``. Raises: libraw.errors.DataError: If invalid or corrupt data is encountered in the data struct. libraw.errors.BadCrop: If the image has been cropped poorly (eg. the edges are outside of the image bounds, or the crop box coordinates don't make sense). libraw.errors.InsufficientMemory: If we run out of memory while processing the raw file. """ self.options._map_to_libraw_params(self.data.contents.params) self.libraw.libraw_dcraw_process(self.data)
[docs] def save(self, filename, filetype=None): """ Save the image data as a new PPM or TIFF image. Args: filename (str): The name of an image file to save. filetype (output_file_types): The type of file to output. By default, guess based on the filename, falling back to PPM. Raises: rawkit.errors.NoFileSpecified: If `filename` is ``None``. rawkit.errors.InvalidFileType: If `filetype` is not None or in :class:`output_file_types`. """ if filename is None: raise NoFileSpecified() if filetype is None: ext = os.path.splitext(filename)[-1].lower()[1:] filetype = ext or output_file_types.ppm if filetype not in output_file_types: raise InvalidFileType( "Output filetype must be in raw.output_file_types") self.data.contents.params.output_tiff = ( filetype == output_file_types.tiff ) self.unpack() self.process() self.libraw.libraw_dcraw_ppm_tiff_writer( self.data, filename.encode('ascii'))
[docs] def save_thumb(self, filename=None): """ Save the thumbnail data. Args: filename (str): The name of an image file to save. Raises: rawkit.errors.NoFileSpecified: If `filename` is ``None``. """ if filename is None: raise NoFileSpecified() self.unpack_thumb() self.libraw.libraw_dcraw_thumb_writer( self.data, filename.encode('ascii'))
@property def color_description(self): """ Get the color_description of an image. Returns: str: 4 character string representing color format, such as 'RGGB'. """ # TODO: remove the pragma once there is integration testing, # but until then testing this is entirely pointless. return self.data.contents.idata.cdesc # pragma: no cover
[docs] def color(self, y, x): """ Get the active color of a pixel of bayer data. Args: y (int): the y coordinate (or row) of the pixel x (int): the x coordinate (or column) of the pixel Returns: str: Character representing the color, such as 'R' for red. """ color_index = self.libraw.libraw_COLOR(self.data, y, x) return self.color_description.decode()[color_index]
@property def color_filter(self): """ EXPERIMENTAL: This method only supports bayer filters for the time being. It will be incorrect when used with other types of sensors. Get the color filter array for the camera sensor. Returns: list: 2D array representing the color format array pattern. For example, the typical 'RGGB' pattern of abayer sensor would be of the format:: [ ['R', 'G'], ['G', 'B'], ] """ # TODO: don't assume 2x2 bayer sensor return [[self.color(0, 0), self.color(0, 1)], [self.color(1, 0), self.color(1, 1)]] @property def color_filter_array(self): """ EXPERIMENTAL: This method only supports bayer filters for the time being. It will be incorrect when used with other types of sensors. Get the color filter array for the camera sensor. Returns: list: Numpy array representing the color format array pattern. For example, the typical 'RGGB' pattern of abayer sensor would be of the format:: array([ ['R', 'G'], ['G', 'B'], ]) """ import numpy return numpy.array(self.color_filter)
[docs] def data_pointer(self): self.unpack() image = self.data.contents.rawdata.raw_image if not bool(image): return None, None sizes = self.data.contents.sizes # TODO: handle this if sizes.pixel_aspect != 1: warnings.warn( "The pixel aspect is not unity, it is: " + str(sizes.pixel_aspect) ) # TODO: handle this if sizes.flip != 0: warnings.warn( "The image is flipped." ) data_pointer = ctypes.cast( image, ctypes.POINTER(ctypes.c_ushort) ) return data_pointer, sizes
[docs] def as_array(self): """ Get a NumPy array of the raw image data Returns: array: A NumPy array of bayer pixel data structured as a list of rows, or array([]) if there is no bayer data. The margin with calibration pixels is always included. For example, if the color format is `RGGB`, the array would be of the format:: array([ [R, G, R, G, ...], [G, B, G, B, ...], [R, G, R, G, ...], ... ]) """ # TODO: Add support for include_margin import numpy data_pointer, sizes = self.data_pointer() if not data_pointer: return numpy.empty((0, 0)) return numpy.ctypeslib.as_array( data_pointer, (sizes.raw_height, sizes.raw_width) )
[docs] def raw_image(self, include_margin=False): """ Get the bayer data for an image if it exists. Args: include_margin (bool): Include margin with calibration pixels. Returns: list: 2D array of bayer pixel data structured as a list of rows, or [] if there is no bayer data. For example, if the color format is `RGGB`, the array would be of the format:: [ [R, G, R, G, ...], [G, B, G, B, ...], [R, G, R, G, ...], ... ] """ data_pointer, sizes = self.data_pointer() if not data_pointer: return [] pitch = sizes.raw_width if include_margin: first = 0 width = sizes.raw_width height = sizes.raw_height else: first = sizes.raw_width * sizes.top_margin + sizes.left_margin width = sizes.width height = sizes.height data = [] for y in range(height): row = [] for x in range(width): row.append(data_pointer[first + y * pitch + x]) data.append(row) return data
[docs] def bayer_data(self, include_margin=False): """ Get the bayer data and color_description for an image. Returns: tuple: Tuple of bayer data and color filter array. This is a convenience method to return `rawkit.raw.Raw.raw_image` and `rawkit.raw.Raw.color_filter_array` as a single tuple. """ return self.raw_image(include_margin), self.color_filter_array
[docs] def to_buffer(self): """ Convert the image to an RGB buffer. Returns: bytearray: RGB data of the image. """ self.unpack() self.process() status = ctypes.c_int(0) processed_image = self.libraw.libraw_dcraw_make_mem_image( self.data, ctypes.cast( ctypes.addressof(status), ctypes.POINTER(ctypes.c_int), ), ) raise_if_error(status.value) data_pointer = ctypes.cast( processed_image.contents.data, ctypes.POINTER(ctypes.c_byte * processed_image.contents.data_size) ) data = bytearray(data_pointer.contents) self.libraw.libraw_dcraw_clear_mem(processed_image) return data
[docs] def thumbnail_to_buffer(self): """ Convert the thumbnail data as an RGB buffer. Returns: bytearray: RGB data of the thumbnail. """ self.unpack_thumb() status = ctypes.c_int(0) processed_image = self.libraw.libraw_dcraw_make_mem_thumb( self.data, ctypes.cast( ctypes.addressof(status), ctypes.POINTER(ctypes.c_int), ), ) raise_if_error(status.value) data_pointer = ctypes.cast( processed_image.contents.data, ctypes.POINTER(ctypes.c_byte * processed_image.contents.data_size) ) data = bytearray(data_pointer.contents) self.libraw.libraw_dcraw_clear_mem(processed_image) return data
@property def metadata(self): """ Common metadata for the photo Returns: rawkit.metadata.Metadata: A metadata object. """ return Metadata( aperture=self.data.contents.other.aperture, timestamp=self.data.contents.other.timestamp, shutter=self.data.contents.other.shutter, flash=bool(self.data.contents.color.flash_used), focal_length=self.data.contents.other.focal_len, height=self.data.contents.sizes.height, iso=self.data.contents.other.iso_speed, make=self.data.contents.idata.make, model=self.data.contents.idata.model, orientation=get_orientation(self.data), width=self.data.contents.sizes.width, )
[docs]class DarkFrame(Raw): """ Represents a dark frame---a raw photo taken in low light which can be subtracted from another photos raw data. Creates a temporary file which is not cleaned up until the dark frame is closed. """ def __init__(self, filename=None): """Initializes a new DarkFrame object.""" super(DarkFrame, self).__init__(filename=filename) self.options = Options({ 'auto_brightness': False, 'brightness': 1.0, 'auto_stretch': True, 'bps': 16, 'gamma': (1, 1), 'rotation': 0, }) self._tmp = os.path.join( tempfile.gettempdir(), '{prefix}{rand}'.format( prefix=tempfile.gettempprefix(), rand=''.join(random.SystemRandom().choice( string.ascii_uppercase + string.digits) for _ in range(8) ) ) ) self._filetype = None
[docs] def save(self, filename=None, filetype=output_file_types.ppm): """ Save the image data, defaults to using a temp file. Args: filename (str): The name of an image file to save. filetype (output_file_types): The type of file to output. Raises: rawkit.errors.InvalidFileType: If `filetype` is not of type :class:`output_file_types`. """ if filename is None: filename = self._tmp if not os.path.isfile(filename): super(DarkFrame, self).save(filename=filename, filetype=filetype)
@property def name(self): """ A tempfile in a unique directory. Returns: str: The name of a temp file. """ return self._tmp
[docs] def cleanup(self): """Cleanup temp files.""" try: os.unlink(self._tmp) except OSError: pass
[docs] def close(self): """Free the underlying raw representation and cleanup temp files.""" super(DarkFrame, self).close() self.cleanup()