@Override
  protected void shutdown() {

    // pause interactor
    pause();

    // stop head movement
    if (headMovement != null) {
      System.out.print("Stopping head movement...");
      headMovement.finish();
      while (!headMovement.isFinished()) {
        try {
          Thread.sleep(500);
        } catch (Exception e) {
          e.printStackTrace();
        }
        System.out.print(".");
      }
      System.out.println("finished");
    }

    // unsubscribe
    synchronized (video) {
      video.unsubscribe(NAME);
    }

    System.exit(0);
  }
  @Override
  protected double[] readInput() {

    // read image
    byte[] imageBinary = null;
    synchronized (video) {
      Variant varImage = video.getImageRemote(NAME);
      Variant varImageElement = varImage.getElement(6);
      imageBinary = varImageElement.toBinary();
    }

    // create pattern
    double[] pattern = new double[ORIGINAL_WIDTH * ORIGINAL_HEIGHT * BITS_PER_PIXEL];
    for (int i = 0; i < pattern.length; i++) {
      int unsignedByte = imageBinary[i] & 0xFF;
      pattern[i] = unsignedByte / 255.0;
    }

    // create image
    BufferedImage image = getBufferedImage(pattern, ORIGINAL_WIDTH, ORIGINAL_HEIGHT, 255);

    // resize image
    BufferedImage imageResized = new BufferedImage(IMAGE_WIDTH, IMAGE_HEIGHT, image.getType());
    Graphics g = imageResized.getGraphics();
    ((Graphics2D) g)
        .setRenderingHint(
            RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
    g.drawImage(image, 0, 0, IMAGE_WIDTH, IMAGE_HEIGHT, null);
    image = imageResized;

    // blur image
    BufferedImage imageBlurred = new BufferedImage(IMAGE_WIDTH, IMAGE_HEIGHT, image.getType());
    GAUSSIAN_FILTER.filter(image, imageBlurred);
    image = imageBlurred;

    // process image
    pattern = imageProcessorRGB.processImageRGB(image);

    // set input pattern
    patternInput = pattern;

    if (!cbLearn.isSelected()) {
      return null;
    }
    return pattern;
  }
  public NAOCameraFeatures() {
    super(NAME, INPUT_DIM);

    // create image processor
    imageProcessorRGB = new ImageProcessorRGB(IMAGE_WIDTH, IMAGE_HEIGHT, NUM_LEVELS);
    System.out.println(
        "Original input has " + (IMAGE_WIDTH * IMAGE_HEIGHT * BITS_PER_PIXEL) + " dimensions");
    System.out.println(
        "Feature vector has "
            + ImageProcessorRGB.getFeatureVectorLength(NUM_LEVELS)
            + " dimensions");

    // set interactor parameters
    setCycleTime(CYCLE_TIME_DEFAULT);
    setInnerFeedback(false);
    setActivateClusterThreshold(2);
    setIterationCountStop(0);

    // set topology parameters
    SOINNM topology = getTopology();
    topology.setNoiseLevel(0.0); // 0.0
    topology.setUseFixedThreshold(true);
    topology.setFixedThreshold(0.1);
    topology.setAgeDead(100);
    topology.setConnectNewNodes(true);
    topology.setLambda(30);
    topology.setEdgeMaxRemoval(true);
    topology.setNodeNumSignalsMinRemoval(true);
    topology.setReduceErrorInsertion(true);
    topology.setSmallClusterRemoval(true);
    topology.setC2Param(0.01);
    topology.setC1Param(0.1);
    topology.setClusterJoining(true);
    topology.setJoinTolerance(1.0);
    topology.setUseAbsoluteJoinTolerance(true);
    topology.setJoinToleranceAbsolute(0.1);
    topology.setJoiningIterationsMax(10);

    // initialise NAO
    try {
      BufferedReader bReader = new BufferedReader(new FileReader("NAOHostPort.txt"));
      host = bReader.readLine().trim();
      port = Integer.parseInt(bReader.readLine().trim());
      bReader.close();
    } catch (Exception e) {
      e.printStackTrace();
    }
    video = new ALVideoDeviceProxy(host, port);
    video.setParam(18, 0); // camera: 0=front, 1=bottom
    video.subscribe(NAME, RESOLUTION, COLOUR_SPACE, FPS);

    // initial adjustment
    if (CAMERA_CALIBRATION) {
      System.out.println("Performing camera calibration...");
      video.setParam(11, 1); // auto exposition (0-1)
      video.setParam(12, 1); // auto white balance (0-1)
      video.setParam(22, 1); // auto exposure correction algorithm (0-1)
      try {
        video.setParam(13, 1); // auto gain (0-1)
        video.setParam(21, 1); // exposure correction (0-1)
        video.setParam(26, 1); // auto balance (0-1)
        video.setParam(27, 128); // auto balance target (0-255)
        video.setParam(28, 128); // auto balance stable range (0-255)		
      } catch (Exception e) {
        System.out.println("Laser head camera model detected!");
      }
      // wait
      try {
        Thread.sleep(CAMERA_CALIBRATION_TIME);
      } catch (Exception e) {
        e.printStackTrace();
      }
    }

    // disable automatic adjustments
    if (CAMERA_DISABLE_ADJUSTMENTS) {
      System.out.println("Disabling automatic camera adjustments...");
      video.setParam(11, 0); // auto exposition (0-1)
      video.setParam(12, 0); // auto white balance (0-1)
      video.setParam(22, 0); // auto exposure correction algorithm (0-1)
      try {
        video.setParam(13, 0); // auto gain (0-1)
        video.setParam(21, 0); // exposure correction (0-1)
        video.setParam(26, 0); // auto balance (0-1)
      } catch (Exception e) {
        System.out.println("Laser head camera model detected!");
      }
      // wait
      try {
        Thread.sleep(CAMERA_DISABLE_ADJUSTMENTS_TIME);
      } catch (Exception e) {
        e.printStackTrace();
      }
    }

    // set exposure and gain manually
    if (CAMERA_MANUAL_EXPOSURE_GAIN) {
      System.out.println("Setting exposure and gain parameters...");
      try {
        video.getParam(13); // cause exception for laser head model
        video.setParam(17, 512); // set exposure (0-4096), 512
        video.setParam(6, 32); // set gain (0-255), 32
      } catch (Exception e) {
        System.out.println("Laser head camera model detected!");
        video.setParam(17, 96); // set exposure (0-512), 96
        video.setParam(6, 48); // set gain (0-255), 48
      }
    }

    // set white balance manually
    if (CAMERA_MANUAL_WHITE_BALANCE) {
      System.out.println("Setting white balance parameters...");
      try {
        video.setParam(4, 80); // red chroma (0-255), 80
        video.setParam(5, 160); // blue chroma (0-255), 160
        video.setParam(25, 64); // auto white balance green gain (0-255), 64
        video.setParam(29, 96); // balance blue (0-255), 96
        video.setParam(30, 96); // balance red (0-255), 96
        video.setParam(31, 96); // balance gain blue (0-255), 96
        video.setParam(32, 96); // balance gain red (0-255), 96
      } catch (Exception e) {
        System.out.println("Laser head camera model detected!");
      }
    }

    // start head movement
    if (MOVE_HEAD) {
      System.out.println("Starting head movement...");
      headMovement = new HeadMovement();
      headMovement.start();
    }

    // create frame
    frame = new JFrame(NAME);
    frame.setSize(600, 600);
    frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
    frame.addWindowListener(
        new WindowAdapter() {
          public void windowClosing(WindowEvent evt) {
            shutdown();
          }
        });
    frame.setLocation(0, 0);
    frame.setVisible(true);

    // create panel
    panel = new JPanel();
    panel.setLayout(new BorderLayout());
    frame.add(panel);

    // create top panel
    JPanel pnTop = new JPanel();
    pnTop.setLayout(new BorderLayout());
    panel.add(pnTop, BorderLayout.PAGE_START);

    // create panel for input and output
    pnIO = new JPanel();
    pnIO.setLayout(new FlowLayout(FlowLayout.LEFT));
    pnIO.setBorder(BorderFactory.createTitledBorder("Input/Output"));

    ImagePanel ipInput = new ImagePanel(IMAGE_BLANK);
    ppInput = new PatternPanel(ipInput, "Input", "");
    pnIO.add(ppInput);

    ImagePanel ipOutput = new ImagePanel(IMAGE_BLANK);
    ppOutput = new PatternPanel(ipOutput, "Output", "");
    pnIO.add(ppOutput);

    pnTop.add(pnIO, BorderLayout.LINE_START);

    // create panel for controls
    JPanel pnControls = new JPanel();
    pnControls.setLayout(new BoxLayout(pnControls, BoxLayout.Y_AXIS));
    pnControls.setBorder(BorderFactory.createTitledBorder("Controls"));
    pnTop.add(pnControls);

    // create panel for interactor
    JPanel pnInteractor = new JPanel();
    pnInteractor.setLayout(new BoxLayout(pnInteractor, BoxLayout.X_AXIS));
    pnControls.add(pnInteractor);

    spCycleTime = new JSpinner(new SpinnerNumberModel((int) CYCLE_TIME_DEFAULT, 100, 1000, 100));
    spCycleTime.setMinimumSize(new Dimension(75, 25));
    spCycleTime.setMaximumSize(new Dimension(75, 25));
    pnInteractor.add(spCycleTime);
    JButton btCycleTime = new JButton("Set Cycle Time");
    btCycleTime.addActionListener(
        new ActionListener() {
          @Override
          public void actionPerformed(ActionEvent e) {
            setCycleTime(Long.parseLong(spCycleTime.getValue().toString()));
          }
        });
    pnInteractor.add(btCycleTime);

    JPanel pnCycleStatus = new JPanel();
    pnCycleStatus.setLayout(new BoxLayout(pnCycleStatus, BoxLayout.Y_AXIS));
    pnInteractor.add(pnCycleStatus);

    lbCycleTime = new JLabel("Cycle Time: " + getCycleTime() + " ms");
    pnCycleStatus.add(lbCycleTime);

    lbCycleDuration = new JLabel("Cycle Duration: - ms");
    pnCycleStatus.add(lbCycleDuration);

    JPanel pnLearnRecall = new JPanel();
    pnLearnRecall.setLayout(new BoxLayout(pnLearnRecall, BoxLayout.X_AXIS));
    pnControls.add(pnLearnRecall);

    cbLearn = new JCheckBox("Learn");
    cbLearn.setSelected(true);
    pnLearnRecall.add(cbLearn);

    cbRecall = new JCheckBox("Recall");
    cbRecall.setSelected(true);
    cbRecall.setEnabled(false);
    pnLearnRecall.add(cbRecall);

    // create panel for topology
    JPanel pnTopology = new JPanel();
    pnTopology.setLayout(new GridLayout(1, 2));
    pnControls.add(pnTopology);

    JPanel pnTopologyStatus = new JPanel();
    pnTopologyStatus.setLayout(new BoxLayout(pnTopologyStatus, BoxLayout.Y_AXIS));
    pnTopology.add(pnTopologyStatus);

    lbNumNodes = new JLabel("Number of Nodes: " + getTopology().getNodeSet().size());
    pnTopologyStatus.add(lbNumNodes);

    lbNumEdges = new JLabel("Number of Edges: " + getTopology().getEdgeSet().size());
    pnTopologyStatus.add(lbNumEdges);

    lbNumClusters = new JLabel("Number of Clusters: " + getTopology().getClusterSet().size());
    pnTopologyStatus.add(lbNumClusters);

    JPanel pnTopologyControls = new JPanel();
    pnTopologyControls.setLayout(new BoxLayout(pnTopologyControls, BoxLayout.Y_AXIS));
    pnTopology.add(pnTopologyControls);

    fcSaveLoad = new JFileChooser();
    fcSaveLoad.setSelectedFile(new File(System.getProperty("user.dir") + "/" + NAME + ".xml"));

    JButton btClear = new JButton("Clear topology");
    btClear.addActionListener(
        new ActionListener() {
          public void actionPerformed(ActionEvent e) {
            getTopology().clear();
          }
        });
    pnTopologyControls.add(btClear);

    JButton btSave = new JButton("Save topology");
    btSave.addActionListener(
        new ActionListener() {
          public void actionPerformed(ActionEvent e) {
            if (fcSaveLoad.showSaveDialog(frame) == JFileChooser.APPROVE_OPTION) {
              fileSave = fcSaveLoad.getSelectedFile();
            }
          }
        });
    pnTopologyControls.add(btSave);

    JButton btLoad = new JButton("Load topology");
    btLoad.addActionListener(
        new ActionListener() {
          public void actionPerformed(ActionEvent e) {
            if (fcSaveLoad.showOpenDialog(frame) == JFileChooser.APPROVE_OPTION) {
              fileLoad = fcSaveLoad.getSelectedFile();
            }
          }
        });
    pnTopologyControls.add(btLoad);

    JButton btInsert = new JButton("Insert topology");
    btInsert.addActionListener(
        new ActionListener() {
          public void actionPerformed(ActionEvent e) {
            if (fcSaveLoad.showOpenDialog(frame) == JFileChooser.APPROVE_OPTION) {
              fileInsert = fcSaveLoad.getSelectedFile();
            }
          }
        });
    pnTopologyControls.add(btInsert);

    // create panel for clusters
    pnClusters = new JPanel();
    pnClusters.setBorder(BorderFactory.createTitledBorder("Clusters"));
    pnClusters.setLayout(new WrapLayout(WrapLayout.LEFT));
    JScrollPane spClusters = new JScrollPane(pnClusters);
    panel.add(spClusters);

    // update panel
    panel.updateUI();
  }