Collective Composite

Dec 1, 2024

The Grid as an expression of Collective Identity

In an era dominated by individual digital self-representation, this project inverts the selfie paradigm by fragmenting and reconstructing portraits into a collective temporal identity. Through the mechanical act of stepping on a switch, participants surrender control over their image's presentation, contributing to a larger, evolving narrative of community and shared experience.

The Grid as Metaphor and Method

The 7x7 grid serves both as a practical framework and a powerful metaphor for how identity is constructed and understood in contemporary society. Each cell represents a fragment of individual identity that, when combined with others, creates something greater than its parts. This systematic fragmentation recalls Mary Kelly's "Post-Partum Document" , where identity was similarly dissected and displayed in grid formations, though here we extend the concept into interactive digital space.

Mary Kelly's "Post-Partum Document" (1973-79)

The grid becomes what Rosalind Krauss terms a "mythic structure" - simultaneously acting as a technical constraint and a conceptual framework that enables new forms of representation. In our installation, it functions as:

  • A structuring device for capturing identity

  • A system for organizing collective contribution

  • A portal between individual and collective experience

Temporal Collective Identity

We placed emphasis on temporal collective identity where meaning emerges through social interaction and participation. As layers of identities accumulate over time, the lightbox becomes a palimpsest of community presence. This temporal aspect is crucial - unlike traditional portraits that capture a single moment, our installation reveals identity as an ongoing process of accumulation and transformation.

The Politics of Visibility and Obscurity

The gradual obscuring of individual portraits as new layers accumulate speaks to what Stuart Hall describes as the "constructed nature of identity". Each new contribution simultaneously reveals and conceals, creating what might be termed a "collective transparency" - where individual identity becomes partially obscured in service of a greater whole. This process is intended to raise questions about:

The relationship between individual and collective representation : The role of chance in identity formation (through random cell assignment), the tension between visibility and invisibility in group identity and the physical accumulation of identity markers over time.

Contemporary Context

This project is meant to respond to and challenge current trends in digital self-representation. While platforms like Instagram emphasize careful curation of individual identity, our installation introduces elements of:

  • Randomness (in cell selection)

  • Loss of control (in image fragmentation)

  • Collective contribution (through layering)

  • Physical manifestation (via thermal printing and lightbox display)

These elements combine to create what might be termed a "collective portrait machine" that questions traditional notions of photographic portraiture and individual identity in the digital age.

Designing the Experience

User Experience

The experience for the installation is an interaction loop that guides participants through capturing, processing, and contributing to the collective portrait:

Participant flow through the exhibition

Initiation

  • Clear visual trigger: Red floor switch

  • Screen displays "Step on the button to begin"

  • Simple, single-action start removes barriers to participation

Capture Sequence

  • Automated countdown with clear visual feedback

  • Flash simulation provides familiar photo-taking cues

  • Real-time preview maintains engagement

Processing & Output

  • Grid overlay shows selected cell

  • Coordinate system (A-G, 1-7) displayed on screen

  • Thermal printer produces individual contribution

  • Receipt includes placement coordinates for reference

Contribution

  • Physical placement on lightbox grid

  • Layer integration with existing contributions

  • System reset for next participant

Affordances & Interface Design

The installation employs several key affordances to guide intuitive interaction:

  1. Physical Triggers

    • High-contrast floor switch

    • Clear spatial relationship between switch, screen, and lightbox

    • Thermal printer positioned for natural flow of movement

  2. Digital Guidance

    • Real-time visual feedback on screen

    • Grid coordinate system matches physical layout

    • Simple alphanumeric system (A-G, 1-7) for placement

  3. Physical Output

    • Thermal printed sticker includes placement coordinates

    • Lightbox grid matches screen display

    • Physical grid lines guide accurate placement.

Interface design and User flow
Overcoming Constraints

The project's constraints served as catalysts for innovative problem-solving, shaping the final design and user experience in significant ways.

Thermal Printer Limitations

One of the key challenges was working within the constraints of the thermal printer, which outputs only in binary black and white, has limited resolution, and requires careful management of contrast. These limitations inspired the development of a sophisticated dithering system to ensure visual coherence.

To address the printer’s binary output, a five-threshold dithering system was implemented:

// Brightness thresholds  
THRESHOLD_VVVLIGHT_GREY = 600;  
THRESHOLD_VVLIGHT_GREY = 500;  
THRESHOLD_VLIGHT_GREY = 400;  
THRESHOLD_LIGHT_GREY = 300;  
THRESHOLD_DARK_GREY = 200;  
THRESHOLD_BLACK = 100;

This system assigns combined RGB values to specific thresholds. Values below 200 render as black, while those between 200 and 300 produce dark grey. These graduated thresholds preserve the essence of portrait recognition, abstracting details while maintaining legibility.

Dithering Effect Progression

Physical Space Considerations

The open floor plan was intentionally designed to facilitate natural queue formation and smooth interaction flow. The proximity of key components enhances usability, while the accessibility of a lightbox for sticker placement ensures an intuitive process.

Social Constraints

Social dynamics were also thoughtfully incorporated into the design. Random cell assignments prevent clustering, ensuring an egalitarian distribution of contributions. Even when stickers are placed incorrectly, the resulting imperfections contribute to the artwork's evolving narrative, highlighting themes of collective behavior and emergent complexity.

Design Implications

The interplay between constraints and affordances generated several critical effects that shaped the experience:

  • Democratic Participation: A single action trigger (a step switch) ensures equal opportunity for engagement. Random cell assignments remove bias, and simple instructions enable broad accessibility.

  • Technical Aesthetics: The dithering system creates a unified visual language, while thermal printing provides immediate, tangible feedback. The grid system offers a structured yet flexible framework for variation.

  • Collective Behavior: The physical act of placing stickers fosters a sense of personal investment in the outcome. The grid system serves as a test of collective adherence to instructions, with the layered contributions revealing intricate community patterns over time.

The overall design balances guidance and creative freedom, using constraints to channel participants' creativity while providing enough structure for a coherent, collective result. This thoughtful integration of limitations transforms potential obstacles into opportunities for artistic exploration and communal expression.

Photo of Installation in use with final product

Technical Design

System Architecture Overview

When designing this interactive installation, we needed a system that could seamlessly capture, process, and display portraits while maintaining user engagement throughout the experience. The core challenge was creating a system that could handle real-time face detection, image processing, and hardware communication while remaining reliable and user-friendly.

System architecture diagram showing data flow

Our solution integrates three main subsystems:

  1. Input Capture

    • Floor-mounted pressure switch for interaction

    • USB camera for portrait capture

    • ML5 face detection for precise positioning

  2. Processing Pipeline

    • Arduino for hardware control

    • Browser-based JavaScript for image processing

    • Custom dithering algorithm for thermal printing

  3. Output Generation

    • Thermal printer for instant feedback

    • LED-lit display grid for collective artwork

    • Real-time preview on display screen

Code Architecture

The system's architecture is designed to handle real-time face detection, image processing, and hardware communication while maintaining a smooth user experience. Each component is carefully structured to work independently while maintaining synchronized operation through event-driven programming.

High level system flow

Our main program flow manages the complex dance between user interaction, face detection, image processing, and hardware communication. The system needs to maintain responsiveness while handling multiple simultaneous operations, from monitoring the pressure switch to processing video frames and managing printer output.

INITIALIZATION:
    Setup Face Detection System
        Initialize ML5 FaceMesh
        Configure Video Capture
        Set Face Detection Options
    
    Setup Grid System
        Create 7x7 Grid Container
        Initialize Grid Sequence Generator
        Setup Grid Display
    
    Setup Serial Communication
        Initialize WebSerial
        Configure Port Settings
        Setup Data Handlers
    
    Setup Display Interface
        Create Timer Display
        Configure Visual Feedback Elements
        Setup Preview System

MAIN PROGRAM FLOW:
    WHILE System Running:
        Monitor Serial Input:
            IF Button Pressed:
                Start Countdown Timer
                Update Visual Display
                
        Monitor Video Feed:
            Track Face Position
            Update Grid Overlay
            Calculate Eye Position for Scaling
            
        WHEN Timer Completes:
            Capture Image
            Process Selected Grid Cell:
                Get Random Grid Position
                Extract Cell Region
                Apply Dithering Algorithm
                Generate Print Preview
            
            Send To Printer:
                Format Coordinates
                Process Image Data
                Transmit Row by Row
                
        Update Display:
            Show Grid Position
            Update Preview
            Show Instructions

DITHERING ALGORITHM:
    FOR Each Pixel in Image:
        Calculate Total RGB Value
        Apply Threshold Levels:
            IF Value < THRESHOLD_DARK_GREY:
                Set Pixel Black
            ELSE IF Value < THRESHOLD_LIGHT_GREY:
                Apply Pattern Based on Position
            ELSE IF Value < THRESHOLD_VLIGHT_GREY:
                Apply Lighter Pattern
            ELSE:
                Set Pixel White
        
        Package For Printer:
            Convert 8 Pixels to One Byte
            Add to Print Buffer

PRINTER COMMUNICATION:
    Send Start Signal ('R')
    FOR Each Row in Image:
        Package Row Data
        Send Row
        Wait for Confirmation
    Send End Signal ('E')
    Send Grid Coordinates

Component Interactions

The system is built around four main components that handle specific aspects of the installation. Each component is designed to be modular, allowing for independent testing and future improvements while maintaining clear communication channels with other parts of the system.

Face Detection and Grid System

The face detection system forms the core of our user interaction, using ML5's FaceMesh to provide real-time tracking and grid alignment. This component ensures consistent portrait capture by dynamically adjusting the grid based on facial features and position.

unction drawFaceGrid(face) {
  // Get eye positions
  let leftEye = face.keypoints[leftEyeKeypoint];  
  let rightEye = face.keypoints[rightEyeKeypoint];
  
  // Calculate eye distance for scaling
  let eyeDistance = dist(leftEye.x, leftEye.y, rightEye.x, rightEye.y);
  
  // Calculate grid size based on eye distance
  let gridWidth = eyeDistance * 2;  // Grid is 3x eye distance wide
  let gridHeight = eyeDistance * 2.5;  // And 3.5x eye distance tall
  
  // Calculate grid position (centered on face)
  let centerX = (leftEye.x + rightEye.x) / 2;
  let centerY = (leftEye.y + rightEye.y) / 2;
  let minX = centerX - (gridWidth / 2);
  let minY = centerY - (gridHeight / 2);
  
  // Calculate box dimensions
  boxWidth = gridWidth / GRID_SIZE;
  boxHeight = gridHeight / GRID_SIZE;
  
  // Draw grid
  stroke(255);
  noFill();
  for (let i = 0; i <= GRID_SIZE; i++) {
    let x = minX + (i * boxWidth);
    line(x, minY, x, minY + gridHeight);
  }
  for (let i = 0; i <= GRID_SIZE; i++) {
    let y = minY + (i * boxHeight);
    line(minX, y, minX + gridWidth, y);
  }
  
  // Number boxes
  textSize(eyeDistance / 12);
  fill(255);
  for (let i = 0; i < GRID_SIZE; i++) {
    for (let j = 0; j < GRID_SIZE; j++) {
      let x = minX + (j * boxWidth);
      let y = minY + (i * boxHeight);
      text(i * GRID_SIZE + j, x + 2, y + 8);
    }
  }
}

Image Processing Pipeline

Our image processing pipeline transforms raw video input into printer-ready output through a series of carefully tuned steps. The dithering algorithm is particularly crucial, as it converts full-color images into binary output while maintaining visual quality through strategic pattern generation.

async function sampleNextBox() {
  // Prevent multiple simultaneous calls
  if (isProcessing) {
    console.log("Already processing or no faces detected");
    return;
  }

Image Processing Pipeline

Our image processing pipeline transforms raw video input into printer-ready output through a series of carefully tuned steps. The dithering algorithm is particularly crucial, as it converts full-color images into binary output while maintaining visual quality through strategic pattern generation.

async function sampleNextBox() {
  // Prevent multiple simultaneous calls
  if (isProcessing) {
    console.log("Already processing or no faces detected");
    return;
  }
  isProcessing = true;
//   console.log(faces.length);
  if (faces.length === 0) return;

  // get reference to the preview container where the dithered preview image will be displayed to the user as the image prints
  const placementPreviewContainer =  document.querySelector("#previewContainer");
  
  // same logic as drawing grid to calculate the grid box to sample from
  let face = faces[0];
  let leftEye = face.keypoints[leftEyeKeypoint];
  let rightEye = face.keypoints[rightEyeKeypoint];
  let eyeDistance = dist(leftEye.x, leftEye.y, rightEye.x, rightEye.y);
  let gridWidth = eyeDistance * 2;
  let gridHeight = eyeDistance * 2.5;
  let centerX = (leftEye.x + rightEye.x) / 2;
  let centerY = (leftEye.y + rightEye.y) / 2;
  let minX = centerX - (gridWidth / 2);
  let minY = centerY - (gridHeight / 2);

  // Calculate box dimensions
  boxWidth = gridWidth / GRID_SIZE;
  boxHeight = gridHeight / GRID_SIZE;
  
  // Get the next box index
  let boxIndex = gridSequence[currentBoxIndex++];
//   console.log("Box Index: " + boxIndex);
//   console.log(getGridCoordinate(GRID_SIZE, boxIndex))
  
  // Calculate the row and column of the box
  let row = floor(boxIndex / GRID_SIZE);
  let col = boxIndex % GRID_SIZE;
  
  // Sample the box
  let x = minX + (col * boxWidth);
  let y = minY + (row * boxHeight);

  // Create image at 384px width 
  sampledImage = createGraphics(PRINTER_IMAGE_WIDTH, PRINTER_IMAGE_HEIGHT);
  sampledImage.copy(video, x, y, boxWidth, boxHeight, 0, 0, PRINTER_IMAGE_WIDTH, PRINTER_IMAGE_HEIGHT);

  // whole image also sampled 
  wholeImage = createGraphics(PRINTER_IMAGE_WIDTH, PRINTER_IMAGE_HEIGHT);
  // copy the entire video feed frame at that moment into wholeimage variable
  wholeImage.copy(video, 0, 0, width, height, 0, 0, PRINTER_IMAGE_WIDTH, PRINTER_IMAGE_HEIGHT);

  // package the rows in such a manner for arduino and thermal printer to recieve and be able to read them respectively
  let packagedRows = packagePrinterData(sampledImage);
  
  // Create printer preview
  let printerPreview = simulatePrinterOutput(sampledImage);

  // display previewDiv canvas to img
  let img = document.createElement('img');
  img.src = printerPreview.elt.toDataURL();

  // Set the size of the img to match the size of the tile div
  img.style.width = '100%';
  img.style.height = '100%';

  // append teh image to the inside of teh tile div but at the correct position
  placementPreviewContainer.innerHTML = '';
  placementPreviewContainer.appendChild(img);

  // place child in correct position on tile of gridContainer
  // append the image to the inside of the tile div but at the correct position
  const tiles = document.querySelectorAll('.tile');
  // go through all tiles and reset background color
  for (let tile of tiles) {
    tile.style.backgroundColor = "rgba(0,0,0,1)";
}
  // change the color of the cell that was just sampled
  if (tiles[boxIndex]) {
    tiles[boxIndex].style.backgroundColor = "rgba(45,45,242,1)"
  }
//   console.log("SENDING ");
  // Send the coordinate data to the printer
  sendCoordinateBitmap(getGridCoordinate(GRID_SIZE, boxIndex));
  // small delay to ensure next serial data does not conflict
  await new Promise(resolve => setTimeout(resolve, 1000));    

  // attempt to send data row by row
  try {
    // let c = 0;
    serial.write('X'); // Send start signal once
    // console.log(packagedRows.length);

    // Send all rows sequentially
    for (let i = 0; i < packagedRows.length; i++) {
        serial.write(packagedRows[i]);
        // console.log("console log row i: " + i);

        // Wait for confirmation of this row
        let confirmed = false;
        const startTime = Date.now();

        while (!confirmed && (Date.now() - startTime) < 2000) {  // 2 second timeout
            await new Promise(resolve => setTimeout(resolve, 10));  // Small delay to ensure message is received and does not overload serial

            if (inString) {
                console.log("rowConfirmIndex:", inString);
                // console.log(inString);
              
                if (inString == 'C') {
                    confirmed = true;
                }
            }
        }

        if (!confirmed) {
            console.log(`Failed to confirm row ${i}`);
            return;  // Stop if we miss a row
        }
        inString = null;
    }
    
    packagedRows = null;
    isProcessing = false;
  } finally {
    isProcessing = false;
  }

  // return the printer preview
  return printerPreview;
}
Early iteration of ML5 to Grid creation

Serial Communication Manager

The serial communication system bridges the digital and physical worlds, managing reliable data transfer between the computer and Arduino. This component handles both input (pressure switch states) and output (image data and printing commands), ensuring robust communication despite the limitations of serial data transfer.

CLASS SerialManager:
    FUNCTION SendImage(processedData):
        Signal Start
        Transmit Row Data
        Signal End
        Send Grid Position
        
    FUNCTION HandleInput():
        Monitor Button State
        Trigger Timer
        Update System State

User Interface

The UI orchestrates the user experience, managing visual feedback and timing to create a cohesive interaction. From countdown timers to preview displays, this component ensures users understand what's happening at each step of the process.

const col = GRID_SIZE;
const row = GRID_SIZE;

// create coordinates
let letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
let numbers = "123456789";
// get reference to teh grid container on DOM
let gridContainer = document.getElementById("gridContainer"); // Assuming you have a container with this ID

// Create a container for the grid with coordinates
let gridWithCoords = document.createElement("div");
// add id to gridWithCoords
gridWithCoords.id = "gridWithCoords";
gridWithCoords.style.display = "grid";
gridWithCoords.style.gridTemplateColumns = `repeat(${col + 1}, auto)`; // +1 for the coordinate column
gridWithCoords.style.gridTemplateRows = `repeat(${row + 1}, auto)`; // +1 for the coordinate row

// Add top x-axis coordinate labels
for (let i = 0; i <= col; i++) {
    let coord = document.createElement("div");
    coord.classList.add("coord");
    if (i > 0) {
        coord.textContent = letters[i - 1];
    }
    gridWithCoords.appendChild(coord);
}

// Add left y-axis coordinate labels and grid tiles
let c = 0;
for (let i = 0; i < row; i++) {
    // Add y-axis coordinate label
    let coord = document.createElement("div");
    coord.classList.add("coord");
    // coord.style.textAlign = "center";
    coord.classList.add("coordNum");
    coord.textContent = numbers[i];
    gridWithCoords.appendChild(coord);

    // Add grid tiles
    for (let j = 0; j < col; j++) {
        let tile = document.createElement("div");
        tile.classList.add("tile");
        tile.id = `${c}`;
        c++;
        gridWithCoords.appendChild(tile);

        // Add border to tiles without having them expand on their size
        tile.style.border = "1px solid white";
        tile.style.display = "inline-block";
        
    }
}

// Append the grid with coordinates to the container
gridContainer.appendChild(gridWithCoords);

//------------
// get references 
const wrapper = document.getElementById("contentWrapper");
const title = document.querySelector("h1");
const squareTimer = document.querySelector(".squareTimer");
const timerText = document.querySelector(".timerText");
const processingScreen = document.querySelector("#processing");
const placementScreen = document.querySelector("#placementScreen");

// create flag for different pages
let experiencing = false;

let timer;
let count;
let duration = 5;
let buttonInput = 0; // Initialize buttonInput

let timerOn = false;


// Watch for changes in buttonInput
function updateTimerState(value) {
    buttonInput = value;
    // console.log(buttonInput);
    if (buttonInput == 1 && !timerOn && !experiencing) {
        experiencing = true;
        startTimer();
    } else if (buttonInput == 0 && timerOn) {
        experiencing = false;
        smoothReset();
    }
}

// Function to start the timer and then decrement the timer number shown on screen
function startTimer() {
    if (!timerOn) {
        timerOn = true;
        count = duration;
        squareTimer.classList.remove('invisible');
        squareTimer.classList.add('visible');

        timerText.textContent = count;

        clearInterval(timer);
        timer = setInterval(() => {
            count--;
            timerText.textContent = count;
            if (count < 0) {
                stopTimer();
            }

        }, 1000);
    }
}

// Function to stop the timer
function stopTimer() {
    clearInterval(timer);
    timerOn = false;
    flashScreen();
    squareTimer.classList.remove('visible');
    squareTimer.classList.add('invisible');
    
}

// function to handle if user stops pressing the button before timer ends
function smoothReset() {
    clearInterval(timer);
    timerOn = false;
    
    // Fade out timer text
    timerText.style.transition = 'opacity 0.2s';
    timerText.style.opacity = '0';
    
    // Fade out overlay
    squareTimer.style.transition = 'opacity 0.2s';
    squareTimer.classList.remove('visible');
    squareTimer.classList.add('invisible');
    
    // Reset after animations complete
    setTimeout(() => {
        count = duration;
        timerText.style.opacity = '1';
        squareTimer.style.transition = 'opacity 1s ease-in-out';
    }, 400);
}

// The flashScreen function to simulate picture being taken
function flashScreen() {
    timerText.textContent = "";
    const overlay = document.createElement("div");
    overlay.style.position = "fixed";
    overlay.style.top = "0";
    overlay.style.left = "0";
    overlay.style.width = "100%";
    overlay.style.height = "100%";
    overlay.style.backgroundColor = "white";
    overlay.style.zIndex = "9999";
    overlay.style.opacity = "1";
    overlay.style.transition = "opacity 1s";

    document.body.appendChild(overlay);

    setTimeout(() => {
        overlay.style.opacity = "0";
    }, 100)

    overlay.addEventListener("transitionend", () => {
        document.body.removeChild(overlay);
        count = duration;
    });
    processingScreen.classList.remove('invisible');
    processingScreen.classList.add('visible');

    sampleNextBox();  
}

// funciton to show the screen that has the preview and instructions for placement of printed image on grid
function placementShow() {
    processingScreen.classList.remove("visible");
    processingScreen.classList.add('invisible');

    placementScreen.classList.remove('invisible');
    placementScreen.classList.add('visible');
    // console.log("doing placement show");

    setTimeout (() => {
        placementScreen.classList.remove('visible');
        placementScreen.classList.add('invisible');

        experiencing = false;
    }, 20000);

}
Physical Computing: Hardware Integration

Component Interaction

The physical system centers around an Arduino Nano 33 IoT that coordinates all hardware interactions:

Fig 1. Arduino to Printer Circuit, Fig 2. Arduino to LED strip with external power supply.

Arduino Control Flow

The Arduino acts as a bridge between the physical and digital components of our installation. Here's the core structure:

SETUP:
    Initialize Serial Communication (115200 baud)
    Configure Thermal Printer
        Set print density, heat settings
        Configure line height
    Initialize Button Input

MAIN LOOP:
    IF Serial Data Available THEN
        Read Command Byte
        
        IF Command is 'C' (Coordinates) THEN
            Read Grid Location (Letter + Number)
            Print Coordinates on Thermal Paper
            
        ELSE IF Command is 'R' (Start Image) THEN
            Reset Image Buffer
            Begin Receiving Image Data
            
        ELSE IF Command is 'E' (End Image) THEN
            Configure High-Quality Print Settings
            Print Bitmap Image
            Feed Paper
            Enter Sleep Mode
            
        ELSE IF Receiving Image Data THEN
            Store Data in Image Buffer Row by Row
            
    CHECK Button State:
        IF Button Pressed AND Not Previously Pressed THEN
            Send "1" to Computer
        IF Button Released AND Previously Pressed THEN
            Send "0" to Computer

The Arduino code manages three critical tasks in our installation:

  1. Button Interaction: It monitors a physical button (our pressure switch) and sends simple "1" or "0" signals to the computer when pressed or released. This trigger starts the entire capture sequence.

  2. Image Reception: When receiving image data from the computer, it uses a buffered approach to handle the large bitmap (368x368 pixels). The data arrives as a series of commands: 'R' to start receiving, rows of image data, and 'E' to end transmission. This chunked approach prevents buffer overflow issues common with large data transfers.

  3. Thermal Printer Control: The system manages precise printer settings for optimal output quality. It handles both text (grid coordinates like "A1") and bitmap data (the actual portrait). The printer settings are carefully tuned with specific heat and timing parameters to ensure clear, dark prints on thermal paper.

Each image row is processed in bytes (8 pixels per byte since thermal printer works in binary black/white), with careful timing management to ensure reliable data transfer. The system includes built-in error checking through byte counting and timeout mechanisms to maintain stability during extended operation.

Bill of Materials

The bill of materials represents a comprehensive breakdown of all components required for our interactive portrait installation. At a total cost of $480, the project components are organized into five main categories: core electronics, lighting components, construction materials, assembly materials, and additional components for safety and maintenance. The core electronics, including the Arduino Nano 33 IoT, thermal printer system, and camera setup, form the technical backbone of the installation. Construction materials, centered around a 19.125" × 19.125" frosted acrylic panel and plywood frame, create the physical structure that houses our interactive elements. We've included detailed listings of smaller but essential items like wire connections and cable management solutions to ensure a professional, safe, and maintainable installation.

Bill of Materials
Fabrication

Push Button

The interactive experience centers on a custom push button that provides reliable tactile feedback. The housing, designed in Fusion 360, consists of two 3D-printed parts that took 6-12 hours to print. The internal mechanism uses four ¾-inch springs fitted into recessed corners, maintaining consistent pressure between two copper contact plates. These plates, secured with hot glue to the inner surfaces, complete a circuit when pressed, sending signals to the Arduino.

Push button assembly

Lightbox

The lightbox combines structural integrity with uniform illumination through careful material selection and assembly. Built around a plywood frame, the box features triangular supports positioned 3mm below the top surface for stability. A 3mm semi-clear acrylic sheet, laser-engraved with our grid pattern, sits flush with the frame edges. The interior uses aluminum foil lining to maximize light diffusion from a 1.5-meter RGB LED strip mounted along the perimeter. Powered by a 5V 3A supply and controlled by an Arduino 33 IoT, the lighting system provides consistent illumination for the interactive display surface.

Lightbox construction details

All components are designed for easy maintenance access while maintaining a clean, professional appearance that focuses attention on the interactive elements of the installation.

Results and Reflections

The Final Experience

The installation successfully created an engaging interactive experience that encouraged participation and collaboration. Users intuitively followed the grid system, stepping on the button and placing their stickers according to the coordinates. As portraits accumulated, the lightbox revealed complex layered patterns, with each new contribution adding to the collective narrative. The thermal printer's dithering effect created consistent, visually appealing portraits that maintained recognizable features while abstract enough to blend into the collective work.

Video of final Experience

Unexpected Outcomes

Several interesting phenomena emerged during the installation's operation. Some users deliberately placed their stickers in incorrect grid positions, creating an unintended commentary on rule-following in collective art. People often formed small groups around the lightbox, discussing portrait placements and creating impromptu stories about the emerging collective image.

Close-up of layered stickers showing temporal effects

Learning Outcomes

This project provided valuable insights across multiple domains. In technical implementation, we gained practical experience with real-time face detection, image processing algorithms, and hardware-software integration. The development of the dithering algorithm taught us about balancing technical constraints with aesthetic quality, while working with thermal printers revealed the importance of precise timing in hardware communication. On the physical computing side, we learned about the challenges of creating reliable interactive interfaces, particularly in designing the pressure switch and managing consistent serial communication.

The social aspects of the installation revealed interesting patterns in collective behavior. Users' interactions with the grid system demonstrated how simple rules can create complex emergent behaviors, while the layering of portraits showed how individual contributions can merge into collective expression. The project also highlighted the importance of clear user feedback and intuitive design in creating engaging interactive experiences.

References
  1. Claude by Anthropic
  2. https://p5js.org/reference
  3. Dithering Tutorial
  4. CSS Loading animation

©2019-2025 SURYA NARREDDI.

©2019-2025 SURYA NARREDDI.