Search this site
Embedded Files
Mason Hawley Ballowe's Landing Page
  • MHB Home Page
  • Reading List
  • Evitable
  • mmm
  • GG
  • PEW
  • QQ
  • FResH
  • Shelter Helper
  • Prof Ballowe : Dev 101
  • Hire The Nerd
  • Wishlist
  • Feens
  • Nerdery
  • Politics
  • Essays
  • Call To Arms
  • Therapy
  • Statements
  • Glossary
Mason Hawley Ballowe's Landing Page
  • MHB Home Page
  • Reading List
  • Evitable
  • mmm
  • GG
  • PEW
  • QQ
  • FResH
  • Shelter Helper
  • Prof Ballowe : Dev 101
  • Hire The Nerd
  • Wishlist
  • Feens
  • Nerdery
  • Politics
  • Essays
  • Call To Arms
  • Therapy
  • Statements
  • Glossary
  • More
    • MHB Home Page
    • Reading List
    • Evitable
    • mmm
    • GG
    • PEW
    • QQ
    • FResH
    • Shelter Helper
    • Prof Ballowe : Dev 101
    • Hire The Nerd
    • Wishlist
    • Feens
    • Nerdery
    • Politics
    • Essays
    • Call To Arms
    • Therapy
    • Statements
    • Glossary

Passive Entry Warden (PEW)

Security With Privacy.

PEW is my home defense app. Use PEW before you pew pew. A future must have for anyone that's self-reliant.

v 1.0 is live.

v 1.1 Pushed fixing a bug that causes the first 5s clip done by the auto-clip tool to not fire. 

v 1.11 will be the sensitivity sliders which will fix the "auto-clip" reset bug. It's not a bug, it's just too sensitive. 

Art by Shiny.

The average home invasion takes less than 10 minutes. The violence portion is usually in the first minute. You likely won't have time to react. Keep this app active (heyoo) at all times, even when you're home. Transparency and evidence are weapons in self-defense. 

Left Camera, no anomaly detected.

Right Camera, anomaly detected.

Intrusion Detection & Reporting

PEW Nerd Speak:

App enables any commercially available webcam to serve as a camera input. App enables auto-creation of video evidence stored locally and in the cloud via webhook (text messages and e-mails draft, but do not send). Custom algorithm monitors every frame of every input and sends to the appropriate output. (select all, deselect all, and manual 1 by 1 allocation of input to output and ouput to input is available.) App enables custom clipping for evidence generation. 

PEW Real Talk:

Set up all your home cameras to record and notify you via your preferred method when any major event happens: motion, light, or camera disconnect. It's completely offline, and since you choose the destination, no Nerd server ever sees your files. 

PEW BiS: 

PEW is the best in class home surveillence for a few reasons:

It works with your existing hardware.

It's completely customizable for your needs without needing a technician.

It doesn't feed any AI, so your home isn't being live-streamed to Epstein/Trump Island. (Did I hear a Flock demo defaulted to a bunch of girls gymnastics locker room, because that was what the presenters were looking at before the test?)

Frame by frame access to my algorithm, which will be posted below once approved by Apple. 

My clipping engine, with both manual and automated capabilities.

All features can be enabled/disabled, including my AI/Algorithm that powers the clipping engine.

It's free.

Features

Wired Camera Inputs

Wireless Camera Inputs

Text Message Outputs

E-mail Outputs

Webhook Outputs

Custom Local Outputs

Input/Output Flow Customization

Frame By Frame Event Detection Algorithm

Low Light Algorithm

Clip Engine

Auto-Clipping with an on/off switch.

Input/Output Persistence

Reset Button

Camera Persistence

Motion Tracker Interface

Backlog

Remote Viewing

Audio

User Algorithm Customization

Low Light Algorithm Customization

Sunrise/Sunset Algorithm

Mobile Compatibility

Use Cases

1: Home Uses:

A neighborhood watch sets up their own homes individually. 

Each home has a private free discord for their internal cameras to automatically send clips. 

Each home has external cameras which auto-send clips to a full community discord server and their own private server. 

A Macbook with a USB hub with a bunch of connected webcams can instantly create a private security network at any location in the world. 

2: Commercial uses:

A restaurant owner connects all cameras and has instant notification when anyone is stealing. So, if you are watching your camera feed and see someone steal, just press the clip +30 button and it'll automatically save the previous 10 seconds (the theft) and the next 30 seconds to whatever output is selected. 

3: Additional Nerds:

This app provides utility. What webhooks will you use, how, and what videos will come out of it? Who will be protected by transparent truth?

4: Additional Value:

Creating a draft e-mail will still upload it to the cloud, depending on your integrations setup. This app enables the creation and distribution of surveillance clips at user discretion. 

I mean, you could all just make better dance videos on my security system, the bar is low.

Privacy Policy

No data is ever sent to any Nerd servers or seen by any Nerd personnel. Obviously that means no data is ever stored in any Nerd servers. 

Payment Policy

It's free. I am not trying to extract a rent on your home security. 

Note: The app is designed with webhooks, and tested via Discord. So, pay them a subscription to host your community. They earned it too.

My goal is to show you the quality of my work, and hope you try my paid products.

My Devices

Airhug Webcam: I chose this because it didn't have a mic and it did have a privacy screen. For the size of the camera, the screen is very rigid and tough to work with... but it is effective. 

Logitech Webcam: I wanted to test a wireless USB camera. Works great :)

Discord: The current auto-clipping algorithm fires one video below the 8MB threshold for Discord's free API and a longer video that will be rejected by most webhooks. An error message will appear to alert the user when a video is rejected by the webhook API. Be sure to give them a Nitro subscription if you use it a lot. 

Stories From The Grind

I think this is a great story on leadership and the setting of metrics; also dumbassery. 

As I was testing the app, I was finally getting the detection algorithm to somewhat work. I hammered it for about a week with several different AI tools to develop a solid comprehensive approach. I started getting really frustrated because as I was testing multiple camera feeds, the webcam kept firing. I was pissed. Why was my webcam firing? Nothing was happening. I was just sitting there. 

I was sitting there. 

It was detecting me. Think Drax thinking he's invisible for a moment.

Early apps, everything breaks until it doesn't. I had gotten so in the habit of everything failing that I was trying to figure out this latest failure without realizing it was my first success on this app. It's amazing how a small shift in perspective can change a failure into a success. 

Dev Notes:

Currently the auto-clip algorithm struggles in low light as a shadow causes something to change from its normal appearance to black very easily. I have added a low light filter to remove false positives but this may remove some actual positives? No algorithm is perfect. 

Sunrise testing has been fun... but it's forecast to rain all week. EFFFFFFFFFFFFF!

V1.0 Detection Algorithm

This will change over time and is imperfect. V1.11 will include a "sensitivity slider" that will allow the user to adjust a few of the coefficients thresholds. I just need to figure out where to place it, as it'll be much more difficult to have sensitivity controls be per-camera-feed as opposed to a universal control as it is now. 


You can copy and paste that and make your own detection algorithm. 


//

//  CheckFrame.swift

//  Passive Entry Warden

//

import AVFoundation

// MARK: - Events

enum CheckFrameEvent {

    case lightChangeDetected(delta: Float)

    case colorChangeDetected(zone: Int)

}

// MARK: - DominantColor

enum DominantColor: String {

    case red    = "Red"

    case orange = "Orange"

    case yellow = "Yellow"

    case green  = "Green"

    case blue   = "Blue"

    case purple = "Purple"

    case white  = "White"

    case gray   = "Gray"

    case black  = "Black"

    static func classify(h: Float, s: Float, v: Float) -> DominantColor {

        if v < 0.15 { return .black }

        if s < 0.15 {

            return v > 0.75 ? .white : .gray

        }

        switch h {

        case 0 ..< 20:    return .red

        case 20 ..< 45:   return .orange

        case 45 ..< 80:   return .yellow

        case 80 ..< 165:  return .green

        case 165 ..< 255: return .blue

        case 255 ..< 330: return .purple

        case 330 ..< 360: return .red

        default:          return .gray

        }

    }

    /// Returns true if two colors are hue-adjacent neighbors that camera

    /// auto-exposure / white-balance can flip between without a real scene change.

    /// Only a jump across non-adjacent colors counts as a genuine change.

    func isAdjacentNeighbor(of other: DominantColor) -> Bool {

        let adjacencies: Set<Set<DominantColor>> = [

            [.red,    .orange],

            [.orange, .yellow],

            [.yellow, .green],

            [.green,  .blue],

            [.blue,   .purple],

            [.purple, .red],      // hue wraps around

            // achromatic neighbors — small saturation shifts can cross these

            [.gray,   .white],

            [.gray,   .black],

            [.white,  .black],

        ]

        return adjacencies.contains([self, other])

    }

}

// MARK: - CheckFrame

struct CheckFrame {

    // MARK: - Tuning Constants

    /// Absolute brightness delta required to fire a global light change (0–1 scale).

    static let brightnessThreshold: Float = 0.020

    /// Relative threshold — delta must also be ≥ this fraction of the baseline.

    static let relativeThreshold: Float = 0.50

    /// Per-zone brightness threshold.

    static let quadrantBrightnessThreshold: Float = 0.090

    /// Minimum fraction of baseline votes a color must win to be considered

    /// a confident baseline. Zones below this are skipped.

    static let colorVoteConfidenceThreshold: Float = 0.75

    /// If the runner-up color received at least this fraction of votes

    /// relative to the winner, the zone is "contested" and skipped.

    /// e.g. 0.60 → skip if 2nd-place ≥ 60% of 1st-place votes.

    static let colorContestThreshold: Float = 0.60

    /// Seconds between color-change events. Light changes are NOT gated.

    static let colorChangeCooldown: TimeInterval = 10.0

    /// Number of frames to sample when computing baselines (~5 s at 30 fps).

    static let baselineFrameCount: Int = 150

    /// Pixels to skip per sample (higher = faster, less precise).

    static let pixelSampleStride: Int = 16

    /// Grid divisions along each axis (10×10 = 100 zones).

    static let gridDivisions: Int = 10

    // MARK: - Public Entry Point

    static func check(_ sampleBuffer: CMSampleBuffer, source: InputSource) {

        let elapsed = Date().timeIntervalSince(source.lastColorChangeTime)

        guard elapsed >= colorChangeCooldown else {

            print("[CheckFrame] \(source.name) — skipping, cooldown \(String(format: "%.1f", elapsed))s")

            return

        }

        guard Date().timeIntervalSince(source.lastColorChangeTime) >= colorChangeCooldown else { return }

        // 1. Sample current frame.

        guard let current = sample(of: sampleBuffer) else { return }

        // 2. Grab baseline frames from ring buffer.

        let recentFrames = source.frameRingBuffer.snapshot().suffix(baselineFrameCount)

        guard recentFrames.count >= 30 else { return }

        // 3. Sample ~30 evenly-spaced baseline frames.

        let strideVal  = max(1, recentFrames.count / 30)

        let zoneCount  = gridDivisions * gridDivisions

        var globalSamples  = [Float]()

        var zoneSamples    = [[Float]](repeating: [], count: zoneCount)

        var zoneColorVotes = [[DominantColor: Int]](repeating: [:], count: zoneCount)

        var zoneTotalVotes = [Int](repeating: 0, count: zoneCount)

        for (idx, buf) in recentFrames.enumerated() {

            guard idx % strideVal == 0 else { continue }

            guard let result = sample(of: buf) else { continue }

            globalSamples.append(result.globalBrightness)

            for z in 0 ..< zoneCount {

                zoneSamples[z].append(result.zoneLuma[z])

                let color = result.zoneDominantColors[z]

                zoneColorVotes[z][color, default: 0] += 1

                zoneTotalVotes[z] += 1

            }

        }

        guard !globalSamples.isEmpty else { return }

        // 4. Global brightness check (absolute + relative).

        let globalBaseline = globalSamples.reduce(0, +) / Float(globalSamples.count)

        let globalDelta    = abs(current.globalBrightness - globalBaseline)

        let globalRelative = globalBaseline * relativeThreshold

        if globalDelta >= brightnessThreshold && globalDelta >= globalRelative {

            handle(.lightChangeDetected(delta: globalDelta), source: source)

            return

        }

        // 5. Per-zone brightness check.

        for z in 0 ..< zoneCount {

            guard !zoneSamples[z].isEmpty else { continue }

            let zBaseline = zoneSamples[z].reduce(0, +) / Float(zoneSamples[z].count)

            let zDelta    = abs(current.zoneLuma[z] - zBaseline)

            if zDelta >= quadrantBrightnessThreshold {

                handle(.lightChangeDetected(delta: zDelta), source: source)

                return

            }

        }

        // 6. Per-zone dominant color check.

        //

        //    A zone fires only when ALL conditions are met:

        //      (a) Cooldown has elapsed since last color event.

        //      (b) Winning baseline color met the confidence threshold (≥75%).

        //      (c) Zone is not "contested" — runner-up < 60% of winner votes.

        //      (d) Current color is NOT an adjacent hue neighbor of the baseline.

        //      (e) Current color is not Black in a well-lit scene (exposure artifact guard).

        guard Date().timeIntervalSince(source.lastColorChangeTime) >= colorChangeCooldown else { return }

        for z in 0 ..< zoneCount {

            guard !zoneColorVotes[z].isEmpty, zoneTotalVotes[z] > 0 else { continue }

            let sorted = zoneColorVotes[z].sorted { $0.value > $1.value }

            let (majorityColor, majorityCount) = sorted[0]

            let confidence = Float(majorityCount) / Float(zoneTotalVotes[z])

            // (b) Confidence gate.

            guard confidence >= colorVoteConfidenceThreshold else { continue }

            // (c) Contest gate.

            if sorted.count >= 2 {

                let runnerUpCount = sorted[1].value

                let contestRatio  = Float(runnerUpCount) / Float(majorityCount)

                if contestRatio >= colorContestThreshold { continue }

            }

            let currentColor = current.zoneDominantColors[z]

            guard currentColor != majorityColor else { continue }

            // (d) Adjacent-neighbor gate — ignore boundary hue flips.

            if currentColor.isAdjacentNeighbor(of: majorityColor) { continue }

            // (e) Skip Black classification in well-lit scenes — likely shadow/exposure artifact.

            if currentColor == .black && current.globalBrightness > 0.25 { continue }

            print("[CheckFrame] \(source.name) — COLOR zone=\(z) baseline=\(majorityColor.rawValue) current=\(currentColor.rawValue) confidence=\(String(format: "%.2f", confidence)) brightness=\(String(format: "%.3f", globalBaseline))")

            handle(.colorChangeDetected(zone: z), source: source)

            return

        }

    }

    // MARK: - Event Handler

    private static func handle(_ event: CheckFrameEvent, source: InputSource) {

        switch event {

        case .lightChangeDetected(let delta):

            print("[CheckFrame] \(source.name) — LIGHT delta=\(String(format: "%.4f", delta))")

            DispatchQueue.main.async {

                source.onLightChangeDetected?()

            }

        case .colorChangeDetected(let zone):

            source.lastColorChangeTime = Date()

            DispatchQueue.main.async {

                source.onColorChangeDetected?(zone)

            }

        }

    }

    // MARK: - Sample Result

    private struct SampleResult {

        let globalBrightness:   Float

        let zoneLuma:           [Float]

        let zoneDominantColors: [DominantColor]

    }

    // MARK: - Pixel Sampling

    private static func sample(of sampleBuffer: CMSampleBuffer) -> SampleResult? {

        guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return nil }

        CVPixelBufferLockBaseAddress(imageBuffer, .readOnly)

        defer { CVPixelBufferUnlockBaseAddress(imageBuffer, .readOnly) }

        let format    = CVPixelBufferGetPixelFormatType(imageBuffer)

        let width     = CVPixelBufferGetWidth(imageBuffer)

        let height    = CVPixelBufferGetHeight(imageBuffer)

        let gd        = gridDivisions

        let zoneCount = gd * gd

        var globalLumaTotal: Float = 0

        var globalCount:     Int   = 0

        var zoneLumaTotals = [Float](repeating: 0, count: zoneCount)

        var zoneRTotals    = [Float](repeating: 0, count: zoneCount)

        var zoneGTotals    = [Float](repeating: 0, count: zoneCount)

        var zoneBTotals    = [Float](repeating: 0, count: zoneCount)

        var zoneCounts     = [Int](repeating: 0,   count: zoneCount)

        // --- YUV formats ---

        if format == kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange ||

           format == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange  ||

           format == kCVPixelFormatType_420YpCbCr8Planar {

            guard let yBase    = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 0),

                  let cbcrBase = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 1)

            else { return nil }

            let yBPR    = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer, 0)

            let cbcrBPR = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer, 1)

            let lumaH   = CVPixelBufferGetHeightOfPlane(imageBuffer, 0)

            let lumaW   = width

            let yPtr    = yBase.assumingMemoryBound(to: UInt8.self)

            let cbcrPtr = cbcrBase.assumingMemoryBound(to: UInt8.self)

            for row in Swift.stride(from: 0, to: lumaH, by: pixelSampleStride) {

                let zRow = min(Int(Float(row) / Float(lumaH) * Float(gd)), gd - 1)

                for col in Swift.stride(from: 0, to: lumaW, by: pixelSampleStride) {

                    let zCol    = min(Int(Float(col) / Float(lumaW) * Float(gd)), gd - 1)

                    let z       = zRow * gd + zCol

                    let Y       = Float(yPtr[row * yBPR + col])

                    let cbcrRow = (row / 2) * cbcrBPR

                    let cbcrCol = (col / 2) * 2

                    let Cb      = Float(cbcrPtr[cbcrRow + cbcrCol])     - 128

                    let Cr      = Float(cbcrPtr[cbcrRow + cbcrCol + 1]) - 128

                    let r = min(max(Y + 1.402  * Cr,              0), 255)

                    let g = min(max(Y - 0.344  * Cb - 0.714 * Cr, 0), 255)

                    let b = min(max(Y + 1.772  * Cb,              0), 255)

                    let luma = 0.299 * r + 0.587 * g + 0.114 * b

                    globalLumaTotal   += luma

                    globalCount       += 1

                    zoneLumaTotals[z] += luma

                    zoneRTotals[z]    += r

                    zoneGTotals[z]    += g

                    zoneBTotals[z]    += b

                    zoneCounts[z]     += 1

                }

            }

        // --- BGRA / RGBA formats ---

        } else if format == kCVPixelFormatType_32BGRA || format == kCVPixelFormatType_32RGBA {

            guard let baseAddress = CVPixelBufferGetBaseAddress(imageBuffer) else { return nil }

            let bpr    = CVPixelBufferGetBytesPerRow(imageBuffer)

            let ptr    = baseAddress.assumingMemoryBound(to: UInt8.self)

            let isBGRA = format == kCVPixelFormatType_32BGRA

            for row in Swift.stride(from: 0, to: height, by: pixelSampleStride) {

                let zRow = min(Int(Float(row) / Float(height) * Float(gd)), gd - 1)

                for col in Swift.stride(from: 0, to: width, by: pixelSampleStride) {

                    let zCol = min(Int(Float(col) / Float(width) * Float(gd)), gd - 1)

                    let z    = zRow * gd + zCol

                    let base = row * bpr + col * 4

                    let r: Float = isBGRA ? Float(ptr[base + 2]) : Float(ptr[base])

                    let g: Float = Float(ptr[base + 1])

                    let b: Float = isBGRA ? Float(ptr[base])     : Float(ptr[base + 2])

                    let luma = 0.299 * r + 0.587 * g + 0.114 * b

                    globalLumaTotal   += luma

                    globalCount       += 1

                    zoneLumaTotals[z] += luma

                    zoneRTotals[z]    += r

                    zoneGTotals[z]    += g

                    zoneBTotals[z]    += b

                    zoneCounts[z]     += 1

                }

            }

        } else {

            return nil

        }

        guard globalCount > 0 else { return nil }

        let globalBrightness = (globalLumaTotal / Float(globalCount)) / 255.0

        var zoneLuma   = [Float](repeating: 0, count: zoneCount)

        var zoneColors = [DominantColor](repeating: .gray, count: zoneCount)

        for z in 0 ..< zoneCount {

            let c = zoneCounts[z]

            guard c > 0 else {

                zoneLuma[z]   = globalBrightness

                zoneColors[z] = .gray

                continue

            }

            let avgR = (zoneRTotals[z] / Float(c)) / 255.0

            let avgG = (zoneGTotals[z] / Float(c)) / 255.0

            let avgB = (zoneBTotals[z] / Float(c)) / 255.0

            zoneLuma[z]   = (zoneLumaTotals[z] / Float(c)) / 255.0

            zoneColors[z] = classifyColor(r: avgR, g: avgG, b: avgB)

        }

        return SampleResult(

            globalBrightness:   globalBrightness,

            zoneLuma:           zoneLuma,

            zoneDominantColors: zoneColors

        )

    }

    // MARK: - Color Classification

    private static func classifyColor(r: Float, g: Float, b: Float) -> DominantColor {

        let maxC  = max(r, g, b)

        let minC  = min(r, g, b)

        let delta = maxC - minC

        let v     = maxC

        let s     = maxC > 0 ? delta / maxC : 0

        var h: Float = 0

        if delta > 0 {

            if maxC == r {

                h = 60 * (((g - b) / delta).truncatingRemainder(dividingBy: 6))

            } else if maxC == g {

                h = 60 * (((b - r) / delta) + 2)

            } else {

                h = 60 * (((r - g) / delta) + 4)

            }

            if h < 0 { h += 360 }

        }

        return DominantColor.classify(h: h, s: s, v: v)

    }

}



#Mason #MasonBallowe #Hawley #Ballowe #MasonHawleyBallowe   #StandUpComedy #sexiestManAlive #handsome #theReadingList  #Masonomics  #evitable  #USAF #veteran #USAFA #usc #book #bookreview    #nerd #dork #geek   #comedian  #mooMain #worldPeace #goofball 
 Mason Hawley Ballowe, Mason, Hawley, Ballowe, Mason Ballowe
Google Sites
Report abuse
Page details
Page updated
Google Sites
Report abuse