After introducing the structure of a camera-based Android application, let’s have a look at the algorithms implemented to decode VLC data
smooth
The smooth function applies a moving average to the data captured by the camera according to the formula
private void smooth() { int[] output = new int[numSamples]; int oldestSample = 0; for (int i=0; i<numSamples; i++) { if (i == 0) { oldestSample = samples[i]; output[i] = samples[i]; } else output[i] = (int)(output[i-1] + (1.0/MOVING_WINDOW_SIZE)*(samples[i]-oldestSample)); if (i >= MOVING_WINDOW_SIZE) oldestSample = samples[i-MOVING_WINDOW_SIZE]; } for (int i=0; i<numSamples; i++) samples[i] = output[i]; }
detectPeaks
This function scans the sampled values looking for peaks. The distances of the peaks gives the frequency of the PWM signal and hence the logical bit being encoded
private void detectPeaks() { numPeaks = 0; peaks = new ArrayList<PeakInfo>(); int[] deltas = new int[numSamples]; // compute deltas deltas[0] = 0; for (int i=1; i<numSamples; i++) deltas[i] = samples[i]-samples[i-1]; for (int i=DELTAS_WINDOW_SIZE; i<numSamples; i+=DELTAS_WINDOW_SIZE) { if (isPositive(deltas, i-DELTAS_WINDOW_SIZE) && isNegative(deltas, i)) { // new peak PeakInfo peak = new PeakInfo(); peak.value = samples[i]; peak.index = i; peaks.add(peak); numPeaks ++; } } }
detectStartOfData
This function loops through the detected peaks looking for the end of the preamble
private int detectStartOfData(int[] idx) { int i; // look for preamble for (i=1; i<numPeaks; i++) { int dist = peaks.get(i).index - peaks.get(i-1).index; if (classifyDistance(dist) == DISTANCE_PREAMBLE) { break; } } // scans until the end of the preamble for (; i<numPeaks; i++) { int dist = peaks.get(i).index - peaks.get(i-1).index; if (classifyDistance(dist) != DISTANCE_PREAMBLE) { idx[0] = i; return peaks.get(i).index; } } return -1; }
detectBit
After the start of data is detected, single bits are decoded. To decode bits, a window is applied in order to eliminate out-of-sync due to the slightly variable frame rate of the smartphone camera
For this reason, only half of the theorical duration of a single bit is checked
In the detectBit function, the peakIndex represents the index in the array of samples (built during the frames capturing phase). startIdx is the index (used as both input and output parameter) in the list of detected peaks. The distances between consecutives peaks in the acquisition window are classified as logical “0” or logical “1”. If the number of peaks detected as “0” or “1” is above a certain threshold, the bit is assumed to be correctly decoded
private int detectBit(int peakIndex, int[] startIdx) { int startIndex = peakIndex + (BIT_WINDOW_SIZE / 4); int endIndex = startIndex + (BIT_WINDOW_SIZE / 2); // look for the first entry after startIndex int peakIdx = startIdx[0]; int index = peaks.get(peakIdx).index; while ((peakIdx < numPeaks) && (index < startIndex)) { peakIdx ++; index = peaks.get(peakIdx).index; } if (peakIdx >= numPeaks) return -1; int numBit1 = 0; int numBit0 = 1; int numOther = 0; peakIdx ++; index = peaks.get(peakIdx).index; while ((peakIdx < numPeaks) && (index <= endIndex)) { int dist = peaks.get(peakIdx).index - peaks.get(peakIdx-1).index; int bit = classifyDistance(dist); if (bit == DISTANCE_BIT_0) numBit0 ++; else if (bit == DISTANCE_BIT_1) numBit1 ++; else numOther ++; peakIdx ++; index = peaks.get(peakIdx).index; } startIdx[0] = peakIdx; if (numBit1 >= MIN_EXPECTED_PEAKS_1) return 1; if (numBit0 >= NUM_EXPECTED_PEAKS_0) return 0; return -1; }
decodeSamples
The above functions are called in the decodeSamples function
private void decodeSamples() { smooth(); detectPeaks(); int[] idx = { 0 }; int peakIndex = detectStartOfData(idx); if (peakIndex == -1) return; String peaks = ""; int value = 0; int bitIdx; for (bitIdx=0; bitIdx<15; bitIdx++) { int bit = detectBit(peakIndex, idx); if (bit == -1) break; peaks += String.format("%d; ", idx); value = (value << 1) | bit; peakIndex += BIT_WINDOW_SIZE; } // check parity (Hamming encoded) // update UI with the decoded value tv3.setText(peaks); if (bitIdx >= 15) tv4.setText(String.format("Value: %04X", value)); }