Thread  RSS Reverse Engineering the PXL-2000



# 13751 9 years ago on Thu, Jun 2 2016 at 1:13 am
Post Image

The PXL-2000 was a kids' camcorder from the late 1980's. I had one that I received as a Christmas present in 1987 and I played with it frequently.

Sitting in my closet for many years, up until 2005, were many cassette tapes including the PXL-2000 tapes. The tapes were high bias chromium dioxide tapes (because they needed a very high frequency response in order to retain the video signal) which had deteriorated over the 15 to 17 years in which they had been in storage.

In 2005, I recorded the raw signals from the tapes to my computer. I no longer had the camera but a regular audio cassette tape deck can play back the signal from the tapes.

The camera's tape speed was around 9 times faster than the normal tape deck speed, which helped tremendously in getting the full signal resolution over to the computer. My sound card at the time was capable of a 92kHz sample rate, so I used that when digitizing the tapes.

Tonight, I finally completed this project. I had tried using Photoshop as a quick and dirty way to view the tape signal (I have an article about this on the main site: user link on www.nitrocosm.com) but the horizontal sync was non-existent. Tonight, however, I used PHP (not the most suitable for the project, perhaps, but it's the programming language with which I have the proficiency) to create a decoder.

I had tried using this program in Linux: user link on sevkeifert.blogspot.com which was written in Java and is pretty amazing. The person who made it is rather brilliant and had many insights into the signal format that I had also found in 2005.

Some of my notes (which were developed independently but closely match Mr. Seifert's findings):

Working with a 192000 kHz sample rate:

1125 samples per line (~170 per second, .006 seconds per line)

16kHz tone for 2250 - 2284 samples (0.012 seconds) - the length of two vertical lines of image

92 lines of video per frame (not including the 16kHz sync tone?)

Each line starts with 30 samples of a "sync mark" (higher amplitude + then -)

This is part of the 1125 samples for each line.

The video resolution is 120x90 pixels with two additional lines of video occupied by the 16 kHz sync tone. The frame rate is 15 frames per second.

After analyzing the signal very carefully in an audio editor, I was not only able to figure out the exact format but I also created, in Photoshop, a mock-up tape signal that also worked perfectly through Mr. Seifert's decoder and also, later on, my PHP-based solution.

The image in this post is one of the frames my decoder extracted. I did not use the first derivative of the signal but, rather, the raw amplitude modulation signal.

Below is a copy of the code for my solution.

<?php 

error_reporting(0);

function readAmp($fd,$pos=0){
  $v = ord($fd{$pos});
  // $v = ($v < 127) ? 127 - $v : $v; // Solarize
  return $v;
}

function drawImg($lines,$filename=false){
  global $finalw, $finalh;
  $h = count($lines);
  $w = count($lines[0]);
  $img = imagecreatetruecolor($w,$h);
  foreach($lines as $y => $line){
    foreach($line as $x => $l){
      $l = ($l > 255) ? 255 : $l;
      $l = ($l < 0) ? 0 : $l;
      $l = intval($l);
      $c = imagecolorallocate($img,$l,$l,$l);
      imagesetpixel($img,$x,$y,$c);
    }    
  }
  $newimg = imagecreatetruecolor($finalw,$finalh);
  imagecopyresampled($newimg,$img,0,0,0,0,$finalw,$finalh,$w,$h);
  if(!empty($filename)){
    imagepng($newimg,$filename);
  } else {   
    header('Content-type: image/png');
    imagepng($newimg);
  }
  imagedestroy($img);
  imagedestroy($newimg);
}

function scaleLine($d,$w){
  if(empty($d)) return false;
  $r = ceil($w / count($d));
  $line = Array();
  if($r > 1){ // If data is smaller than desired line width (scale up)
    for($i=0;$i<$w;$i++){
      for($k=0;($k<$r && $k<$w);$k++){
        if(isset($d[$i])) $line[] = $d[$i];
      }
    }
  } else { // If data is larger than desired line width (scale down)
    for($i=0;$i<$w;$i++){
      if(isset($d[$i])) $line[] = $d[$i]; // TODO:  Do scaling later.
    }

  }
  return $line;  
}

function scaleLineExact($d,$w){
  if(empty($d)) return false;
  $ow = count($d);
  $img = imagecreatetruecolor($ow,1);
  for($i=0;$i<$ow;$i++){
    $l = $d[$i];
    $l = ($l > 255) ? 255 : $l;
    $l = ($l < 0) ? 0 : $l;
    $l = intval($l);
    $c = imagecolorallocate($img,$l,$l,$l);
    imagesetpixel($img,$i,0,$c);
  }
  $d = Array(); // Reset pixel data
  $newimg = imagecreatetruecolor($w,1);
  imagecopyresampled($newimg,$img,0,0,0,0,$w,1,$ow,1);
  imagedestroy($img);
  for($i=0;$i<$w;$i++){
    $c = imagecolorat($newimg,$i,0);
    $d[] = ($c & 0xFF); // Just use blue channel since all channels are equal values.
  }
  return $d;
}

$finalw = 640;
$finalh = 480;

$fh = fopen('./pxl2keqd.raw','r');
$fl = 106320;
$fpos = 0;

$fo = 0;

$line_limit = 96; // Signal vertical resolution 

$threshold = 10;
$min_line_length = 500;
$max_line_length = 1200;

while($fd = fread($fh,$fl)){

  $lines = Array();
  $line = Array();
  $cpos = 0;

  for($i=0;$i<$fl;$i++){
    $p = readAmp($fd,$i);
    $fpos++;
    $cpos++;
    $delta = ($i > 0) ? abs($p - readAmp($fd,$i-1)) : 0;
    if(($delta < $threshold) && (count($line) < $max_line_length)){
      $line[] = $p;
    } else if(count($line) > $min_line_length){    
      $lines[] = scaleLineExact($line,$max_line_length);
      $line = Array();
      if(count($lines) >= $line_limit){
        $fpos = $fpos - ($fl - $cpos);
        fseek($fh,$fpos);
        break;
      }
    }  
  }

  $filename = 'output/'.sprintf("%02d",$fo).'.png';
  
  drawImg($lines,$filename);
  
  $fo++;

}

?>

(This post was edited 9 years ago on Thursday, June 2nd, 2016 at 7:28 am)

73's, KD8FUD

User Image

# 13752 9 years ago on Thu, Jun 2 2016 at 1:20 am

Also, here is the article where Mr. Seifert breaks down the technical details of the PXL-2000 signal: user link on sevkeifert.blogspot.com

The only thing that seems odd is that the video signal for the PXL-2000, despite what's apparently in the patent, doesn't actually seem to be FM, at least not for the tapes from my old camcorder.

Regarding the above code, I should add that the data with which the code works is raw (headerless) 8-bit, unsigned, and resampled to a 192 kHz sample rate. It determines when a new line starts based on the extremity of an amplitude change between two consecutive samples. I'm actually rather surprised it works, to be honest.

This is the video, which I made from the output frames using mencoder.

Also, the patent from 1986: user link on www.google.com

(This post was edited 9 years ago on Thursday, June 2nd, 2016 at 7:34 am)

73's, KD8FUD

User Image

# 13753 9 years ago on Thu, Jun 2 2016 at 10:15 pm

Interesting that PHP is used to do this instead of Python or Java.

I can see... something in the picture but it looks like just a couple of dark areas and nothing else in the frame but static. Furthermore, most of the image looks like a neutral grey instead of favoring a light or dark background.

Did you ever figure out what it is that's shown in the video?

For me, it is far better to grasp the Universe as it really is than to persist in delusion, however satisfying and reassuring. -- Carl Sagan

# 13754 9 years ago on Fri, Jun 3 2016 at 3:42 am

Well, I've been messing about with it some more.

On Friday, June 3rd, 2016 at 3:15 am, Jovian said:

I can see... something in the picture but it looks like just a couple of dark areas and nothing else in the frame but static. Furthermore, most of the image looks like a neutral grey instead of favoring a light or dark background.

Did you ever figure out what it is that's shown in the video?

I changed the way the signal was interpreted and made some other fine adjustments.

It's a little better and I think this is how the video is actually encoded. High values (values further from 127 - 127 is the "zero" value since I'm working with an unsigned input file) and 255 and 0 are the maximum values. Anything below 127 is now inverted.

I'd messed around with using the deltas (y(x) - y(x-1)) instead of the raw values but that just looked way too pixelated and staticky to be correct. The signal doesn't seem to be encoded that way.

Here is the revised code:

<?php 

error_reporting(0);

function readAmp($fd,$pos=0){
  $v = ord($fd{$pos});
  // $v = ($v < 127) ? 127 - $v : $v; // Solarize
  return $v;
}

function drawImg($lines,$filename=false){
  global $finalw, $finalh;
  $h = count($lines);
  $w = count($lines[0]);
  $img = imagecreatetruecolor($w,$h);
  foreach($lines as $y => $line){
    foreach($line as $x => $l){
      $l = ($l > 255) ? 255 : $l;
      $l = ($l < 0) ? 0 : $l;
      $l = intval($l);
      $c = imagecolorallocate($img,$l,$l,$l);
      imagesetpixel($img,$x,$y,$c);
    }    
  }
  $newimg = imagecreatetruecolor($finalw,$finalh);
  imagecopyresampled($newimg,$img,0,0,0,0,$finalw,$finalh,$w,$h);
  if(!empty($filename)){
    imagepng($newimg,$filename);
  } else {   
    header('Content-type: image/png');
    imagepng($newimg);
  }
  imagedestroy($img);
  imagedestroy($newimg);
}

function scaleLine($d,$w){
  if(empty($d)) return false;
  $r = ceil($w / count($d));
  $line = Array();
  if($r > 1){ // If data is smaller than desired line width (scale up)
    for($i=0;$i<$w;$i++){
      for($k=0;($k<$r && $k<$w);$k++){
        if(isset($d[$i])) $line[] = $d[$i];
      }
    }
  } else { // If data is larger than desired line width (scale down)
    for($i=0;$i<$w;$i++){
      if(isset($d[$i])) $line[] = $d[$i]; // TODO:  Do scaling later.
    }

  }
  return $line;  
}

function scaleLineExact($d,$w){
  if(empty($d)) return false;
  $ow = count($d);
  $img = imagecreatetruecolor($ow,1);
  for($i=0;$i<$ow;$i++){
    $l = $d[$i];
    $l = ($l > 255) ? 255 : $l;
    $l = ($l < 0) ? 0 : $l;
    $l = intval($l);
    $c = imagecolorallocate($img,$l,$l,$l);
    imagesetpixel($img,$i,0,$c);
  }
  $d = Array(); // Reset pixel data
  $newimg = imagecreatetruecolor($w,1);
  imagecopyresampled($newimg,$img,0,0,0,0,$w,1,$ow,1);
  imagedestroy($img);
  for($i=0;$i<$w;$i++){
    $c = imagecolorat($newimg,$i,0);
    $d[] = ($c & 0xFF); // Just use blue channel since all channels are equal values.
  }
  return $d;
}

$finalw = 640;
$finalh = 480;

$fh = fopen('./pxl2keqd.raw','r');
$fl = 106320;
$fpos = 0;

$fo = 0;

$line_limit = 96; // Signal vertical resolution 

$threshold = 10;
$min_line_length = 500;
$max_line_length = 1200;

while($fd = fread($fh,$fl)){

  $lines = Array();
  $line = Array();
  $cpos = 0;

  for($i=0;$i<$fl;$i++){
    $p = readAmp($fd,$i);
    $fpos++;
    $cpos++;
    $delta = ($i > 0) ? abs($p - readAmp($fd,$i-1)) : 0;
    // $delta2 = ($i > 4) ? round((abs($p - readAmp($fd,$i-1)) + abs($p - readAmp($fd,$i-2)) + abs($p - readAmp($fd,$i-3)) + abs($p - readAmp($fd,$i-4))) / 4) : 0; // Experimental.  Average of past four deltas.
    $delta2 = ($i > 4) ? abs((abs($p - readAmp($fd,$i-1)) - abs($p - readAmp($fd,$i-4)))) : 0; // Experimental.  Difference between (t-1) and (t-4).
    if(($delta < $threshold) && (count($line) < $max_line_length)){
      // $line[] = $p;
      //$line[] = round((1/$delta2) * 255); // Experimental.  Using the averages of the past four deltas instead of immediate input value.
      $p = $delta2;
      $v = ($p > 127) ? $p : 255 - $p;
      $v = ($v * 2) - 255;
      $line[] = $v;
    } else if(count($line) > $min_line_length){    
      $lines[] = scaleLineExact($line,$max_line_length);
      $line = Array();
      if(count($lines) >= $line_limit){
        $fpos = $fpos - ($fl - $cpos);
        fseek($fh,$fpos);
        break;
      }
    }  
  }

  $filename = 'output/'.sprintf("%02d",$fo).'.png';
  
  drawImg($lines,$filename);
  
  $fo++;

}

?>

(This post was edited 9 years ago on Friday, June 3rd, 2016 at 4:59 am)

73's, KD8FUD

User Image

# 13755 9 years ago on Sat, Jun 4 2016 at 12:17 am

That's really cool!

I say the signal on the tape is degraded so badly that it's almost totally unrecognizable but it does look like you're at least getting some picture.

That's a cool toy - a kids' camcorder that records video (granted, very low quality video) on a regular audio cassette.

Your PHP code looks pretty straightforward but I wonder if there's something you could do about the vertical sync?

"Dangerous toys are fun, but you could get hurt!"


Return to Index Return to topic list

Forgot password?
Currently Online
Users:0
Guests:8

Most Recently Online
Nitrocosm2 weeks ago
ZOL2 months ago
Wolfwood292 months ago
lam2 months ago
Jovian2 months ago