array('Application Entity',16,0), 'AS' => array('Age String',4,1), 'AT' => array('Attribute Tag',4,1), 'CS' => array('Code String',16,0), 'DA' => array('Date',8,1), 'DS' => array('Decimal String',16,0), 'DT' => array('Date Time',26,0), 'FL' => array('Floating Point Single',4,1), 'FD' => array('Floating Point Double',8,1), 'IS' => array('Integer String',12,0), 'LO' => array('Long String',64,0), 'LT' => array('Long Text',10240,0), 'OB' => array('Other Byte String',0,0), 'OX' => array('Mixed. Other {Byte|Word} String',0,0), 'OW' => array('Other Word String',0,0), 'PN' => array('Person Name',64,0), 'SH' => array('Short String',16,0), 'SL' => array('Signed Long',4,1), 'SQ' => array('Sequence of Items',0,0), 'SS' => array('Signed Short',2,1), 'ST' => array('Short Text',1024,0), 'TM' => array('Time',16,0), 'UI' => array('Unique Identifier UID',64,0), 'UL' => array('Unsigned Long',4,1), 'UN' => array('Unknown',0,0), 'US' => array('Unsigned Short',2,1), 'UT' => array('Unlimited Text',0,0) ); /** * This class allows reading and modifying of DICOM files */ class File_DICOM extends PEAR { /** * DICOM dictionary. * @var array */ var $dict; /** * Flag indicating if the current file is a DICM file or a NEMA file. * true => DICM, false => NEMA. * * @var bool */ var $is_dicm; /** * Currently open file. * @var string */ var $current_file; /** * Initial 0x80 bytes of last read file * @var string */ var $_preamble_buff; /** * Array of DICOM elements indexed by group and element index * @var array */ var $_elements; /** * Array of DICOM elements indexed by name * @var array */ var $_elements_by_name; /** * Transfer Syntax. To be able of reading different types according to standard * 1.2.840.10008.1.2 Implicit VR, Little Endian * 1.2.840.10008.1.2.1 Explicit VR, Little Endian * 1.2.840.10008.1.2.2 Explicit VR, Big Endian * @var string */ var $transfer_syntax; /** * Encapsulated Transfer Syntax. * 1.2.840.10008.1.2.4.50 JPEG baseline * 1.2.840.10008.1.2.4.51 JPEG extended * 1.2.840.10008.1.2.4.57 JPEG lossless, non-hierarchical * 1.2.840.10008.1.2.4.70 JPEG lossless, non-hierarchical, first-order prediction * @var string */ var $_encapsulated_transfer_syntax = array( "1.2.840.10008.1.2.4.50", "1.2.840.10008.1.2.4.51", "1.2.840.10008.1.2.4.57", "1.2.840.10008.1.2.4.70" ); /** * Constructor. * It creates a File_DICOM object. * * @access public */ function File_DICOM() { /** * Initialize dictionary. */ global $dictionary; $this->dict = $dictionary->dict; } /** * Utility to know if a file is DICOM or not. * It is not a bullet-proof test. * * @access public * @param string $infile The DICOM file to parse * @return boolean true if file is DICOM, or false is file does not satisfy the preamble test. * Notice, that a file returning false could still be DICOM. */ function isDicom($infile) { return $this->parse($infile, true); } /** * Parse a DICOM file * Parse a DICOM file and get all of its header members * * @access public * @param string $infile The DICOM file to parse * @param boolean $only_check_type Default to false. It is true when called by method isDicom * @return mixed true on success or DICOM file, PEAR_Error on failure, or false when is not a DICOM file */ function parse($infile, $only_check_type = false) { $this->current_file = $infile; // Fill the transfer syntax by default to Explicit VR Little Endian $this->transfer_syntax = EXPLICIT_VR_LITTLE_ENDIAN; $fh = @fopen($infile, "rb"); if (!$fh) { return $this->raiseError("Could not open file $infile for reading"); } $stat = fstat($fh); $this->_file_length = $stat[7]; // Test for NEMA or DICOM file. // If DICM, store initial preamble and leave file ptr at 0x84. $this->_preamble_buff = fread($fh, 0x80); $buff = fread($fh, 4); $this->is_dicm = ($buff == 'DICM'); if ($only_check_type) { fclose($fh); return $this->is_dicm; } if (!$this->is_dicm) { fseek($fh, 0); } $last_group = 0x0000; // Fill in hash with header members from given file. while (ftell($fh) < $this->_file_length) { $element =& new File_DICOM_Element($fh, $this->dict,$this->transfer_syntax,$last_group); $this->_elements[$element->group][$element->element][] =& $element; $this->_elements_by_name[$element->name][] =& $element; $last_group = $element->group; // Fill the proper transfer syntax if ($element->group == 0x0002 && $element->element == 0x0010) { $this->transfer_syntax = $element->value; } } fclose($fh); return true; } /** * Write current contents to a DICOM file. * * @access public * @param string $outfile The name of the file to write. If not given * it assumes the name of the file parsed. * If no file was parsed and no name is given * returns a PEAR_Error * @return mixed true on success, PEAR_Error on failure */ function write($outfile = '') { if ($outfile == '') { if (isset($this->current_file)) { $outfile = $this->current_file; } else { return $this->raiseError("File name not given (and no file currently open)"); } } $fh = @fopen($outfile, "wb"); if (!$fh) { return $this->raiseError("Could not open file $outfile for writing"); } // Writing file from scratch will always fail for now if (!isset($this->_preamble_buff)) { return $this->raiseError("Cannot write DICOM file from scratch"); } // Don't store initial preamble and DICM word. Working with NEMA. //fwrite($fh, $this->_preamble_buff); //fwrite($fh, 'DICM'); /*$buff = fread($fh, 4); $this->is_dicm = ($buff == 'DICM'); if (!$this->is_dicm) { fseek($fh, 0); }*/ // There are not that much groups/elements. Using foreach() foreach (array_keys($this->_elements) as $group) { foreach (array_keys($this->_elements[$group]) as $element) { foreach (array_keys($this->_elements[$group][$element]) as $z) { fwrite($fh, pack('v', $group)); fwrite($fh, pack('v', $element)); $code = $this->_elements[$group][$element][$z]->code; // Preserve the VR type from the file parsed if (($this->_elements[$group][$element][$z]->vr_type == FILE_DICOM_VR_TYPE_EXPLICIT_32_BITS) or ($this->_elements[$group][$element][$z]->vr_type == FILE_DICOM_VR_TYPE_EXPLICIT_16_BITS)) { fwrite($fh, pack('CC', $code{0}, $code{1})); // This is an OB, OW, SQ, UN or UT: 32 bit VL field. if ($this->_elements[$group][$element][$z]->vr_type == FILE_DICOM_VR_TYPE_EXPLICIT_32_BITS) { fwrite($fh, pack('V', $this->_elements[$group][$element][$z]->length)); } else { // not using fixed length from VR!!! fwrite($fh, pack('v', $this->_elements[$group][$element][$z]->length)); } } elseif ($this->_elements[$group][$element][$z]->vr_type == FILE_DICOM_VR_TYPE_IMPLICIT) { fwrite($fh, pack('V', $this->_elements[$group][$element][$z]->length)); } switch ($code) { // Decode unsigned longs and shorts. case 'UL': fwrite($fh, pack('V', $this->_elements[$group][$element][$z]->value)); break; case 'US': fwrite($fh, pack('v', $this->_elements[$group][$element][$z]->value)); break; // Floats: Single and double precision. case 'FL': fwrite($fh, pack('f', $this->_elements[$group][$element][$z]->value)); break; case 'FD': fwrite($fh, pack('d', $this->_elements[$group][$element][$z]->value)); break; // Binary data. Only save position. Is this right? case 'OW': case 'OB': case 'OX': // Binary data. Value only contains position on the parsed file. // Will fail when file name for writing is the same as for parsing. $fh2 = @fopen($this->current_file, "rb"); if (!$fh2) { return $this->raiseError("Could not open file {$this->current_file} for reading"); } fseek($fh2, $this->_elements[$group][$element][$z]->value); fwrite($fh, fread($fh2, $this->_elements[$group][$element][$z]->length)); fclose($fh2); break; default: fwrite($fh, $this->_elements[$group][$element][$z]->value, $this->_elements[$group][$element][$z]->length); break; } } } } fclose($fh); return true; } /** * Gets the value for a DICOM element * Gets the value for a DICOM element of a given group from the * parsed DICOM file. * * @access public * @param mixed $gp_or_name The group the DICOM element belongs to * (integer), or it's name (string) * @param integer $el The identifier for the DICOM element * (unique inside a group) * @param integer $z The 3rd dimension for the element (created for sequences) * By default is set to 0 for backwards compatibility * (usually for single elements). * @return mixed The value for the DICOM element on success, PEAR_Error on failure */ function getValue($gp_or_name, $el = null, $z = 0) { if (isset($el)) // retreive by group and element index { if (isset($this->_elements[$gp_or_name][$el][$z])) { $value = $this->_elements[$gp_or_name][$el][$z]->getValue(); if (empty($value)) { return null; } return $this->_elements[$gp_or_name][$el][$z]->getValue(); } else { $this->raiseError("Element ($gp_or_name,$el) not found"); return null; } } else // retrieve by name { if (isset($this->_elements_by_name[$gp_or_name][$z])) { $value = $this->_elements_by_name[$gp_or_name][$z]->getValue(); if (empty($value)) { return null; } return $this->_elements_by_name[$gp_or_name][$z]->getValue(); } else { $this->raiseError("Element $gp_or_name not found"); return null; } } } /** * Sets the value for a DICOM element * Only works with strings now. * * @access public * @param integer $gp The group the DICOM element belongs to * @param integer $el The identifier for the DICOM element (unique inside a group) * @param integer $z The index of the element. By default 0 (for single elements) */ function setValue($gp, $el, $value, $z = 0) { $this->_elements[$gp][$el][$z]->value = $value; $this->_elements[$gp][$el][$z]->length = strlen($value); } /** * Dumps the contents of the image inside the DICOM file * (element 0x0010 from group 0x7FE0) to a PGM (Portable Gray Map) file. * Use with Caution!!. For a 8.5MB DICOM file on a P4 it takes 28 * seconds to dump it's image. * * @access public * @param string $filename The file where to save the image * @return mixed true on success, PEAR_Error on failure. */ function dumpImage($filename) { $length = $this->_elements[0x7FE0][0x0010]->length; $rows = $this->getValue(0x0028,0x0010); $cols = $this->getValue(0x0028,0x0011); $fh = @fopen($filename, "wb"); if (!$fh) { return $this->raiseError("Could not open file $filename for writing"); } // magick word fwrite($fh, "P5\n"); // comment fwrite($fh, "# file generated by PEAR::File_DICOM on ".strftime("%Y-%m-%d", time())." \n"); fwrite($fh, "# do not use for diagnosing purposes\n"); fwrite($fh, "$cols $rows\n"); // always 255 grays fwrite($fh, "255\n"); $pos = $this->getValue(0x7FE0,0x0010); $fh_in = @fopen($this->current_file, "rb"); if (!$fh_in) { return $this->raiseError("Could not open file {$this->current_file} for reading"); } fseek($fh_in, $pos); $block_size = 4096; $blocks = ceil($length / $block_size); for ($i = 0; $i < $blocks; $i++) { if ($i == $blocks - 1) { // last block $chunk_length = ($length % $block_size) ? ($length % $block_size) : $block_size; } else { $chunk_length = $block_size; } $chunk = fread($fh_in, $chunk_length); $pgm = ''; $half_chunk_length = $chunk_length/2; $rr = unpack("v$half_chunk_length", $chunk); for ($j = 1; $j <= $half_chunk_length; $j++) { $pgm .= pack('C', $rr[$j] >> 4); } fwrite($fh, $pgm); } fclose($fh_in); fclose($fh); return true; } /** * Creates an array of GD image object. * * @access public * @param string $filename The file where to save the image * @return mixed image object array on success, * PEAR_Error on failure or NULL if GD is not configured. */ function imagecreatefromDICOM($filename, $window = null, $level = null) { if (!function_exists('imagecreatetruecolor')) { return NULL; } // Let's read some values from DICOM file $length = $this->_elements[0x7FE0][0x0010][0]->length; $rows = $this->getValue(0x0028, 0x0010); $cols = $this->getValue(0x0028, 0x0011); $samples_per_pixel = $this->getValue(0x0028, 0x0002); $bits = $this->getValue(0x0028, 0x0100); $high_bit = $this->getValue(0x0028, 0x0102); $dose_scaling = (empty($this->_elements[0x3004][0x000E][0]->value))? 1 : $this->getValue(0x3004, 0x000E); $window_center = (empty($this->_elements[0x0028][0x1050][0]->value))? 0 : $this->getValue(0x0028, 0x1050); $window_width = (empty($this->_elements[0x0028][0x1051][0]->value))? 0 : $this->getValue(0x0028, 0x1051); $rescale_intercept = (empty($this->_elements[0x0028][0x1052][0]->value))? 0 : $this->getValue(0x0028, 0x1052); $rescale_slope = (empty($this->_elements[0x0028][0x1053][0]->value))? 1 : $this->getValue(0x0028, 0x1053); $number_of_frames = (empty($this->_elements[0x0028][0x0008][0]->value))? 1 : $this->getValue(0x0028, 0x0008); $pixel_representation = $this->getValue(0x0028, 0x0103); $photometric_interpretation = $this->getValue(0x0028, 0x0004); $starting_position = $this->getValue(0x7FE0, 0x0010); $transfer_syntax_uid = $this->getValue(0x0002, 0x0010); //$this->_elements = null; if (in_array($transfer_syntax_uid, $this->_encapsulated_transfer_syntax)) { // Sorry, encapsulated data is more complex to display... by now. return NULL; } if ($rows * $cols >= 500000) { // Sorry, file is too big, conver it in a different way return NULL; } // Window Center and Width can have multiple values. By now, just reading the first one. It assumes the delimiter // is the "\" if (!(strpos($window_center,"\\") === false)) { $temp = explode("\\",$window_center); $window_center = intval($temp[0]); } if (!(strpos($window_width,"\\") === false)) { $temp = explode("\\",$window_width); $window_width = intval($temp[0]); } // Opening the file $fh = fopen ($filename, 'rb'); if (!$fh) { return $this->raiseError("Could not open file $filename for writing"); } fseek($fh, $starting_position); // $data holds the pixel values $maximum = array(); $minimum = array(); $images = array(); $data = array(); $max = array(); $min = array(); $current_position = $starting_position; $current_image = 0; $bytes_to_read = $bits/8; $size_image = $cols * $rows * $bytes_to_read * $samples_per_pixel; $length = $size_image * $number_of_frames; while (ftell($fh) < $starting_position + $length) { if ($current_position == $starting_position + $current_image*$size_image) { $x = 0; $y = 0; for ($i = 0; $i < $samples_per_pixel; $i++) { $max[$current_image][$i] = -20000; // Small enough so it will be properly calculated $min[$current_image][$i] = 20000; // Large enough so it wil be properly calculated } $current_image++; } for ($i = 0; $i < $samples_per_pixel; $i++) { $chunk = fread($fh, $bytes_to_read); $current_position += $bytes_to_read; $pixel_value = base_convert(bin2hex(strrev($chunk)), 16, 10); // Now we have the pixel value // Checking if 2's complement $pixel_value = ($pixel_representation)? $this->complement2($pixel_value, $high_bit) : $pixel_value; // Getting the right value according to slope and intercept $pixel_value = $pixel_value*$rescale_slope + $rescale_intercept; // Multiplying for dose_grid_scaling $pixel_value = $pixel_value*$dose_scaling; // Assigning the value $data[$current_image - 1][$x][$y][$i] = $pixel_value; // Getting the max if ($pixel_value > $max[$current_image - 1][$i]) { $max[$current_image - 1][$i] = $pixel_value; // max } // Getting the min if ($pixel_value < $min[$current_image - 1][$i]) { $min[$current_image - 1][$i] = $pixel_value; // min } } $y++; if ($y == $cols) { // Next row $x++; $y=0; } } fclose ( $fh ); for ($index = 0; $index < $current_image; $index++) { for ($i = 0; $i < $samples_per_pixel; $i++) { // Real max and min according to window center & width (if set) $maximum[$i] = ($window_center != 0 && $window_width != 0)? round($window_center + $window_width/2) : $max[$index][$i]; $minimum[$i] = ($window_center != 0 && $window_width != 0)? round($window_center - $window_width/2) : $min[$index][$i]; // Check if window and level are sent $maximum[$i] = (!empty($window) && !empty($level))? round($level + $window/2) : $maximum[$i]; $minimum[$i] = (!empty($window) && !empty($level))? round($level - $window/2) : $minimum[$i]; //echo $index . " " . $maximum . " " . $minimum . "
\n"; if ($maximum[$i] == $minimum[$i]) { // Something wrong. Avoid having a zero division return NULL; } } $img = imagecreatetruecolor($cols, $rows); for($x =0; $x < $rows;$x++) { for ($y = 0; $y < $cols; $y++) { $pixel_values = array(); for ($i = 0; $i < $samples_per_pixel; $i++) { $pixel_value = $data[$index][$x][$y][$i]; // truncating pixel values over max and below min $pixel_value = ($pixel_value > $maximum[$i])? $maximum[$i] : $pixel_value; $pixel_value = ($pixel_value < $minimum[$i])? $minimum[$i] : $pixel_value; // Converting to gray value $pixel_value = ($pixel_value - $minimum[$i])/($maximum[$i] - $minimum[$i])*255; // For MONOCHROME1 we have to invert the pixel values. if ($photometric_interpretation == "MONOCHROME1") { $pixel_value = 255 - $pixel_value; } $pixel_values[$i] = $pixel_value; } if ($samples_per_pixel == 1) { $color = imagecolorallocate($img, $pixel_values[0], $pixel_values[0], $pixel_values[0]); } else { $color = imagecolorallocate($img, $pixel_values[0], $pixel_values[1], $pixel_values[2]); } imagesetpixel($img, $y, $x, $color); } } $images[] = $img; } return $images; } /** * Return an integer that was saved as complement 2 . * * @access public * @param integer $number The integer number to convert * @param integer $high_bit The high bit for the value. By default 15 (assumes 2 bytes) * @return integer the number after complement's 2 applied */ function complement2($number, $high_bit = 15) { $sign = $number >> $high_bit; if ($sign) { // Negative $number = pow(2, $high_bit + 1) - $number; $number = -1 * $number; } return $number; } /** * Returns an html string with the DICOM file parsed. * * @access public * @param object $dicom_element A Dicom element. It could be a sequence too. * @param string $prefix To have a nice display at the beginning of each line * @return string An html string */ function htmlParse($dicom_element="", $prefix="Element") { if ($dicom_element == "") { $dicom_element = $this; } $data = ""; $count = 1; if (count($dicom_element->_elements) > 0) { foreach (array_keys($dicom_element->_elements) as $group) { // Loading groups foreach (array_keys($dicom_element->_elements[$group]) as $element) { // Loading elements foreach ($dicom_element->_elements[$group][$element] as $thisElement) { // Loading items in element (Sequences or single element) $value = ($thisElement->name == "UNKNOWN")? "Propriatory": $thisElement->value; $data .= " $prefix.$count - ".$thisElement->name . " [" . $this->strHex($group) . "][" . $this->strHex($element) . "] (Length: ". $thisElement->length . ") = " . $value . "
"; $data .= $this->htmlParse($thisElement,"$prefix.$count"); $count++; } } } } return $data; } /** * Returns an string as 0x0000. This might be done also using sprintf or alike but not so familiar with that. * * @access public * @param integer $value An integer value. Either the group or element number. * @return string $string An string with the format 0x0000. */ function strHex($value) { $string = "0x" . str_pad(strtoupper(dechex($value)), 4, "0", STR_PAD_LEFT); return $string; } } ?>