import warnings
import os
import numpy as np
import types
from typing import BinaryIO
from .dng import Tag, dngIFD, dngTag, DNG, DNGTags
from .defs import Compression, DNGVersion, SampleFormat
from .packing import *
from .camdefs import BaseCameraModel
class DNGBASE:
def __init__(self) -> None:
self.compress = None
self.path = None
self.tags = None
self.filter = None
def __data_condition__(self, data : np.ndarray) -> None:
if data.dtype != np.uint16 and data.dtype != np.float32:
raise Exception("RAW Data is not in correct format. Must be uint16_t or float32_t Numpy Array. ")
def __tags_condition__(self, tags : DNGTags) -> None:
if not tags.get(Tag.ImageWidth):
raise Exception("No width is defined in tags.")
if not tags.get(Tag.ImageLength):
raise Exception("No height is defined in tags.")
if not tags.get(Tag.BitsPerSample):
raise Exception("Bit per pixel is not defined.")
def __unpack_pixels__(self, data : np.ndarray) -> np.ndarray:
return data
def __filter__(self, rawFrame: np.ndarray, filter : types.FunctionType) -> np.ndarray:
if not filter:
return rawFrame
processed = filter(rawFrame)
if not isinstance(processed, np.ndarray):
raise TypeError("return value is not a valid numpy array!")
elif processed.shape != rawFrame.shape:
raise ValueError("return array does not have the same shape!")
if processed.dtype != np.uint16:
raise ValueError("array data type is invalid!")
return processed
def __process__(self, rawFrame : np.ndarray, tags: DNGTags, compress : bool,
file : BinaryIO = None) -> bytearray:
width = tags.get(Tag.ImageWidth).rawValue[0]
length = tags.get(Tag.ImageLength).rawValue[0]
bpp = tags.get(Tag.BitsPerSample).rawValue[0]
compression_scheme = Compression.LJ92 if compress else Compression.Uncompressed
sample_format = SampleFormat.Uint
backward_version = DNGVersion.V1_0
if rawFrame.dtype == np.float32:
sample_format = SampleFormat.FloatingPoint
# Floating-point data requires DNG 1.4
backward_version = DNGVersion.V1_4
# Floating-point data has to be compressed with deflate
if compress:
raise Exception('Compression is not supported for floating-point data')
if compress:
from ljpegCompress import pack16tolj
tile = pack16tolj(rawFrame, int(width*2),
int(length/2), bpp, 0, 0, 0, "", 6)
else:
if bpp == 8:
packedFrame = rawFrame.astype('uint8')
elif bpp == 10:
packedFrame = pack10(rawFrame)
elif bpp == 12:
packedFrame = pack12(rawFrame)
elif bpp == 14:
packedFrame = pack14(rawFrame)
else:
# 16-bit integers or 32-bit floats
packedFrame = rawFrame
# These buffers are all contiguous, so the optimised output route
# can use the underlying memoryview, no need to convert to bytes.
tile = packedFrame.data if file else packedFrame.tobytes()
dngTemplate = DNG()
dngTemplate.ImageDataStrips.append(tile)
# set up the FULL IFD
mainIFD = dngIFD()
mainTagStripOffset = dngTag(
Tag.StripOffsets, [0 for tile in dngTemplate.ImageDataStrips])
mainIFD.tags.append(mainTagStripOffset)
mainIFD.tags.append(dngTag(Tag.NewSubfileType, [0]))
byte_counts = [tile.nbytes if isinstance(tile, memoryview) else len(tile)
for tile in dngTemplate.ImageDataStrips]
mainIFD.tags.append(dngTag(Tag.StripByteCounts, byte_counts))
mainIFD.tags.append(dngTag(Tag.Compression, [compression_scheme]))
mainIFD.tags.append(dngTag(Tag.Software, "PiDNG"))
mainIFD.tags.append(dngTag(Tag.DNGVersion, DNGVersion.V1_4))
mainIFD.tags.append(dngTag(Tag.DNGBackwardVersion, backward_version))
mainIFD.tags.append(dngTag(Tag.SampleFormat, [sample_format]))
for tag in tags.list():
try:
mainIFD.tags.append(tag)
except Exception as e:
print("TAG Encoding Error!", e, tag)
dngTemplate.IFDs.append(mainIFD)
totalLength = dngTemplate.dataLen()
mainTagStripOffset.setValue(
[k for offset, k in dngTemplate.StripOffsets.items()])
buf = bytearray(totalLength)
dngTemplate.setBuffer(buf)
# The file parameter will cause the optimised output route to be used,
# where appropriate.
dngTemplate.write(file=file)
return buf
def options(self, tags : DNGTags, path : str, compress=False) -> None:
self.__tags_condition__(tags)
self.tags = tags
self.compress = compress
self.path = path
def convert(self, image : np.ndarray, filename="", file : BinaryIO = None):
# The file parameter can be passed an open file handle, or a BytesIO,
# and this function will take an optimised route to writing the output.
# Note that the pixel data is not copied to self.buf (which is why it's
# faster) in this case.
if self.tags is None:
raise Exception("Options have not been set!")
# valdify incoming data
self.__data_condition__(image)
unpacked = self.__unpack_pixels__(image)
filtered = self.__filter__(unpacked, self.filter)
buf = self.__process__(filtered, self.tags, self.compress, file=file)
if file:
# For the optimised output route, __process__ has already written
# the output for us, so we are finished.
return
file_output = False
if len(filename) > 0:
file_output = True
if file_output:
if not filename.endswith(".dng"):
filename = filename + '.dng'
outputDNG = os.path.join(self.path, filename)
with open(outputDNG, "wb") as outfile:
outfile.write(buf)
return outputDNG
else:
return buf
class RAW2DNG(DNGBASE):
def __init__(self) -> None:
super().__init__()
class CAM2DNG(DNGBASE):
def __init__(self, model : BaseCameraModel) -> None:
super().__init__()
self.model = model
def options(self, path : str, compress=False) -> None:
self.__tags_condition__(self.model.tags)
self.tags = self.model.tags
self.compress = compress
self.path = path
class RPICAM2DNG(CAM2DNG):
def __data_condition__(self, data : np.ndarray) -> None:
if data.dtype != np.uint8:
warnings.warn("RAW Data is not in correct format. Already unpacked? ")
def __unpack_pixels__(self, data : np.ndarray) -> np.ndarray:
if data.dtype != np.uint8:
return data
width, height = self.model.fmt.get("size", (0,0))
stride = self.model.fmt.get("stride", 0)
bpp = self.model.fmt.get("bpp", 8)
# check to see if stored packed or unpacked format
if "CSI2P" in self.model.fmt.get("format", ""):
s_bpp = bpp # stored_bitperpixel
else:
s_bpp = 16
bytes_per_row = int(width * (s_bpp / 8))
data = data[:height, :bytes_per_row]
if s_bpp == 10:
data = data.astype(np.uint16) << 2
for byte in range(4):
data[:, byte::5] |= ((data[:, 4::5] >> ((byte+1) * 2)) & 0b11)
data = np.delete(data, np.s_[4::5], 1)
elif s_bpp == 12:
data = data.astype(np.uint16)
shape = data.shape
unpacked_data = np.zeros((shape[0], int(shape[1] / 3 * 2)), dtype=np.uint16)
unpacked_data[:, ::2] = (data[:, ::3] << 4) + (data[:, 2::3] & 0x0F)
unpacked_data[:, 1::2] = (data[:, 1::3] << 4) + ((data[:, 2::3] >> 4) & 0x0F)
data = unpacked_data
elif s_bpp == 16:
data = np.ascontiguousarray(data).view(np.uint16)
return data
class PICAM2DNG(RPICAM2DNG):
"""For use within picamera2 library"""
def options(self, compress=False) -> None:
self.__tags_condition__(self.model.tags)
self.tags = self.model.tags
self.compress = compress
self.path = ""