Java Tutorial

Lesson 4: Toy Edge Detect Library

Image matrices, grayscale conversion, convolution, Sobel edge detection, pixel math, normalization, clamped sampling, and PNG output.

Run

scripts/run.sh lesson_4 demo
scripts/run.sh lesson_4 exercise
scripts/apply_answers.sh lesson_4
scripts/run.sh lesson_4 verify
scripts/reset_exercises.sh lesson_4

Function Pages

67 pages generated for Java functions in this lesson.

View related functions

Lesson README

Lesson 4: Toy Edge Detect Image Preprocess Library

This lesson builds a small Java library around image preprocessing, convolution, and Sobel edge detection.

120-Minute Plan

MinutesActivity
0-10Frame images as matrices and compare Java arrays with array libraries in other ecosystems.
10-30Run scripts/run.sh lesson_4 demo and inspect generated PNG files under build/out/lesson_4.
30-50Read ImageMatrix, Convolution, and EdgeDetector.
50-70Discuss bounds handling, clamping, grayscale conversion, and kernel design.
70-100Implement pixel math exercise functions.
100-115Apply answers and compare numerical choices.
115-120Sketch a library API change, such as adding blur before edge detection.

Exercise

Use workbook.md for the full two-hour learner path. It includes image tracing, numeric implementation rounds, and boundary edge cases.

Edit src/exercise/java/tutorial/lesson4/exercise/PixelMath.java.

Functions to implement:

The toy library itself is runnable independent of the exercise answers.

After applying answers, run scripts/run.sh lesson_4 verify to check byte validation, luma math, packed pixels, normalization, blending, Sobel clamping, rectangular image validation, and boundary sampling.

Learner Workbook

Learner Workbook: Lesson 4 Toy Edge Detect Image Library

This workbook is designed to take about two hours. The goal is to understand Java library structure and image preprocessing through small numeric functions.

What You Should Understand By The End

0-15 Minutes: Generate Images

Run:

scripts/reset_exercises.sh lesson_4
scripts/run.sh lesson_4 demo
scripts/run.sh lesson_4 exercise

Open:

build/out/lesson_4/synthetic-input.png
build/out/lesson_4/sobel-edges.png

Write down what changed between the input and edge output.

15-35 Minutes: Trace The Library

Open src/demo/java/tutorial/lesson4/demo/EdgeDetectDemo.java.

Trace:

Then open:

For ImageMatrix, pay attention to the difference between (x, y) coordinates and values[y][x] storage.

35-50 Minutes: Read The Exercise API

Open src/exercise/java/tutorial/lesson4/exercise/PixelMath.java.

Group functions by skill:

Before editing, write expected outputs for at least four functions.

50-95 Minutes: Implement In Rounds

Round 1:

Round 2:

Round 3:

Round 4:

Round 5:

Round 6:

Round 7:

Round 8:

Round 9:

95-110 Minutes: Verify Answers

Run:

scripts/apply_answers.sh lesson_4
scripts/run.sh lesson_4 verify
scripts/run.sh lesson_4 demo
scripts/reset_exercises.sh lesson_4

The verifier checks numerical edge cases that are easy to miss by visual inspection alone.

110-120 Minutes: Edge-Case Drill

Try these cases:

Reset when done:

scripts/reset_exercises.sh lesson_4

Instructor Notes

Instructor Notes: Lesson 4 Toy Edge Detect Image Preprocess Library

Session Goal

Teach Java library organization through a small image preprocessing library. Learners should understand package layout, matrix-style data structures, grayscale conversion, convolution kernels, Sobel edge detection, file I/O, and numerical helper functions.

Learning Outcomes

By the end of 120 minutes, learners should be able to:

Before Class

Run:

scripts/reset_exercises.sh lesson_4
scripts/run.sh lesson_4 demo
scripts/run.sh lesson_4 exercise

Open the generated files:

Know the key files:

Minute-By-Minute Plan

TimeSegmentInstructor Moves
0-10OrientationAsk learners how images are represented in tools they know. Establish pixels as numeric samples.
10-25Run the demoRun scripts/run.sh lesson_4 demo and open the generated PNG files.
25-40Entry point readingOpen EdgeDetectDemo.java. Trace input selection, output paths, and library calls.
40-58Image matrixOpen ImageMatrix.java. Explain width, height, row-major storage, grayscale conversion, and clamped sampling.
58-75ConvolutionOpen Kernel.java and Convolution.java. Trace one 3x3 sample by hand.
75-88SobelOpen EdgeDetector.java. Explain blur, horizontal gradient, vertical gradient, and magnitude.
88-105Exercise workLearners implement the staged pixel-math and boundary-handling lab from workbook.md.
105-115Solution compareApply answers, run the exercise, and discuss rounding choices.
115-120ExtensionAsk learners where they would add thresholding, blur strength, or command-line options.

Teaching Script

Start with:

"This is a toy library, not a full image framework. That is intentional. The smaller surface lets us see Java package design and numeric processing without extra dependencies."

For ImageMatrix:

Checkpoint question:

"What bug would appear if we indexed values[x][y] instead?"

For convolution:

Explain that every output pixel is a weighted sum of neighboring input pixels. The kernel is centered over the current pixel. Boundary handling matters because the kernel extends past the image edge.

For Sobel:

Exercise Facilitation

Learners edit PixelMath.java.

Recommended order:

Hints:

Expected Answer Behavior

After applying answers:

scripts/apply_answers.sh lesson_4
scripts/run.sh lesson_4 verify
scripts/run.sh lesson_4 demo

Expected important lines:

lesson_4 answers verified

The demo should also write:

build/out/lesson_4/synthetic-input.png
build/out/lesson_4/sobel-edges.png

Common Mistakes

Discussion Prompts

Extension Challenge

Ask learners to add a threshold step that turns edge magnitudes above 100 white and everything else black. Then ask whether thresholding should be part of EdgeDetector.sobel, a separate preprocessing function, or a caller option.

Functions In This Lesson

PixelMath.PixelMath

lesson_4/answers/java/tutorial/lesson4/exercise/PixelMath.java:4 | constructor | answer

private PixelMath() {

PixelMath.clamp

lesson_4/answers/java/tutorial/lesson4/exercise/PixelMath.java:7 | method | answer

public static int clamp(int value, int min, int max) {

PixelMath.invertGray

lesson_4/answers/java/tutorial/lesson4/exercise/PixelMath.java:14 | method | answer

public static int invertGray(int gray) {

PixelMath.threshold

lesson_4/answers/java/tutorial/lesson4/exercise/PixelMath.java:19 | method | answer

public static int threshold(int gray, int cutoff) {

PixelMath.luma

lesson_4/answers/java/tutorial/lesson4/exercise/PixelMath.java:25 | method | answer

public static int luma(int rgb) {

PixelMath.packGray

lesson_4/answers/java/tutorial/lesson4/exercise/PixelMath.java:32 | method | answer

public static int packGray(int gray) {

PixelMath.normalizeToByte

lesson_4/answers/java/tutorial/lesson4/exercise/PixelMath.java:37 | method | answer

public static int normalizeToByte(double value, double min, double max) {

PixelMath.blend

lesson_4/answers/java/tutorial/lesson4/exercise/PixelMath.java:50 | method | answer

public static int blend(int leftGray, int rightGray, double alpha) {

PixelMath.sobelMagnitude

lesson_4/answers/java/tutorial/lesson4/exercise/PixelMath.java:59 | method | answer

public static int sobelMagnitude(int gx, int gy) {

PixelMath.sampleClamped

lesson_4/answers/java/tutorial/lesson4/exercise/PixelMath.java:63 | method | answer

public static int sampleClamped(int[][] image, int x, int y) {

PixelMath.mean3x3

lesson_4/answers/java/tutorial/lesson4/exercise/PixelMath.java:70 | method | answer

public static int mean3x3(int[][] image, int centerX, int centerY) {

PixelMath.requireByte

lesson_4/answers/java/tutorial/lesson4/exercise/PixelMath.java:81 | method | answer

private static void requireByte(int value, String name) {

PixelMath.validateImage

lesson_4/answers/java/tutorial/lesson4/exercise/PixelMath.java:87 | method | answer

private static void validateImage(int[][] image) {

EdgeDetectDemo.EdgeDetectDemo

lesson_4/src/demo/java/tutorial/lesson4/demo/EdgeDetectDemo.java:10 | constructor | demo

private EdgeDetectDemo() {

EdgeDetectDemo.main

lesson_4/src/demo/java/tutorial/lesson4/demo/EdgeDetectDemo.java:13 | method | demo

public static void main(String[] args) throws IOException {

Convolution.Convolution

lesson_4/src/demo/java/tutorial/lesson4/image/Convolution.java:4 | constructor | demo

private Convolution() {

Convolution.apply

lesson_4/src/demo/java/tutorial/lesson4/image/Convolution.java:7 | method | demo

public static ImageMatrix apply(ImageMatrix input, Kernel kernel) {

EdgeDetector.EdgeDetector

lesson_4/src/demo/java/tutorial/lesson4/image/EdgeDetector.java:4 | constructor | demo

private EdgeDetector() {

EdgeDetector.sobel

lesson_4/src/demo/java/tutorial/lesson4/image/EdgeDetector.java:7 | method | demo

public static ImageMatrix sobel(ImageMatrix input) {

ImageIo.ImageIo

lesson_4/src/demo/java/tutorial/lesson4/image/ImageIo.java:9 | constructor | demo

private ImageIo() {

ImageIo.readGrayscale

lesson_4/src/demo/java/tutorial/lesson4/image/ImageIo.java:12 | method | demo

public static ImageMatrix readGrayscale(Path path) throws IOException {

ImageIo.writeGrayscale

lesson_4/src/demo/java/tutorial/lesson4/image/ImageIo.java:16 | method | demo

public static void writeGrayscale(ImageMatrix matrix, Path path) throws IOException {

ImageMatrix.ImageMatrix

lesson_4/src/demo/java/tutorial/lesson4/image/ImageMatrix.java:11 | constructor | demo

public ImageMatrix(int width, int height) {

ImageMatrix.width

lesson_4/src/demo/java/tutorial/lesson4/image/ImageMatrix.java:20 | method | demo

public int width() {

ImageMatrix.height

lesson_4/src/demo/java/tutorial/lesson4/image/ImageMatrix.java:24 | method | demo

public int height() {

ImageMatrix.get

lesson_4/src/demo/java/tutorial/lesson4/image/ImageMatrix.java:28 | method | demo

public double get(int x, int y) {

ImageMatrix.getClamped

lesson_4/src/demo/java/tutorial/lesson4/image/ImageMatrix.java:32 | method | demo

public double getClamped(int x, int y) {

ImageMatrix.set

lesson_4/src/demo/java/tutorial/lesson4/image/ImageMatrix.java:38 | method | demo

public void set(int x, int y, double value) {

ImageMatrix.syntheticTestPattern

lesson_4/src/demo/java/tutorial/lesson4/image/ImageMatrix.java:42 | method | demo

public static ImageMatrix syntheticTestPattern(int width, int height) {

ImageMatrix.fromBufferedImage

lesson_4/src/demo/java/tutorial/lesson4/image/ImageMatrix.java:54 | method | demo

public static ImageMatrix fromBufferedImage(BufferedImage image) {

ImageMatrix.toBufferedImage

lesson_4/src/demo/java/tutorial/lesson4/image/ImageMatrix.java:69 | method | demo

public BufferedImage toBufferedImage() {

ImageMatrix.clampToByte

lesson_4/src/demo/java/tutorial/lesson4/image/ImageMatrix.java:81 | method | demo

private static int clampToByte(double value) {

Kernel.Kernel

lesson_4/src/demo/java/tutorial/lesson4/image/Kernel.java:4 | compact constructor | demo

public Kernel {

Kernel.width

lesson_4/src/demo/java/tutorial/lesson4/image/Kernel.java:16 | method | demo

public int width() {

Kernel.height

lesson_4/src/demo/java/tutorial/lesson4/image/Kernel.java:20 | method | demo

public int height() {

Kernel.at

lesson_4/src/demo/java/tutorial/lesson4/image/Kernel.java:24 | method | demo

public double at(int x, int y) {

Kernel.sobelX

lesson_4/src/demo/java/tutorial/lesson4/image/Kernel.java:28 | method | demo

public static Kernel sobelX() {

Kernel.sobelY

lesson_4/src/demo/java/tutorial/lesson4/image/Kernel.java:36 | method | demo

public static Kernel sobelY() {

Kernel.boxBlur3

lesson_4/src/demo/java/tutorial/lesson4/image/Kernel.java:44 | method | demo

public static Kernel boxBlur3() {

AnswerVerifier.AnswerVerifier

lesson_4/src/exercise/java/tutorial/lesson4/exercise/AnswerVerifier.java:4 | constructor | exercise

private AnswerVerifier() {

AnswerVerifier.main

lesson_4/src/exercise/java/tutorial/lesson4/exercise/AnswerVerifier.java:7 | method | exercise

public static void main(String[] args) {

AnswerVerifier.expectEquals

lesson_4/src/exercise/java/tutorial/lesson4/exercise/AnswerVerifier.java:47 | method | exercise

private static void expectEquals(int expected, int actual, String label) {

AnswerVerifier.expectThrows

lesson_4/src/exercise/java/tutorial/lesson4/exercise/AnswerVerifier.java:53 | method | exercise

private static void expectThrows(Runnable action, String label) {

ExerciseRunner.ExerciseRunner

lesson_4/src/exercise/java/tutorial/lesson4/exercise/ExerciseRunner.java:4 | constructor | exercise

private ExerciseRunner() {

ExerciseRunner.main

lesson_4/src/exercise/java/tutorial/lesson4/exercise/ExerciseRunner.java:7 | method | exercise

public static void main(String[] args) {

PixelMath.PixelMath

lesson_4/src/exercise/java/tutorial/lesson4/exercise/PixelMath.java:4 | constructor | exercise

private PixelMath() {

PixelMath.clamp

lesson_4/src/exercise/java/tutorial/lesson4/exercise/PixelMath.java:7 | method | exercise

public static int clamp(int value, int min, int max) {

PixelMath.invertGray

lesson_4/src/exercise/java/tutorial/lesson4/exercise/PixelMath.java:13 | method | exercise

public static int invertGray(int gray) {

PixelMath.threshold

lesson_4/src/exercise/java/tutorial/lesson4/exercise/PixelMath.java:18 | method | exercise

public static int threshold(int gray, int cutoff) {

PixelMath.luma

lesson_4/src/exercise/java/tutorial/lesson4/exercise/PixelMath.java:23 | method | exercise

public static int luma(int rgb) {

PixelMath.packGray

lesson_4/src/exercise/java/tutorial/lesson4/exercise/PixelMath.java:29 | method | exercise

public static int packGray(int gray) {

PixelMath.normalizeToByte

lesson_4/src/exercise/java/tutorial/lesson4/exercise/PixelMath.java:34 | method | exercise

public static int normalizeToByte(double value, double min, double max) {

PixelMath.blend

lesson_4/src/exercise/java/tutorial/lesson4/exercise/PixelMath.java:40 | method | exercise

public static int blend(int leftGray, int rightGray, double alpha) {

PixelMath.sobelMagnitude

lesson_4/src/exercise/java/tutorial/lesson4/exercise/PixelMath.java:46 | method | exercise

public static int sobelMagnitude(int gx, int gy) {

PixelMath.sampleClamped

lesson_4/src/exercise/java/tutorial/lesson4/exercise/PixelMath.java:51 | method | exercise

public static int sampleClamped(int[][] image, int x, int y) {

PixelMath.mean3x3

lesson_4/src/exercise/java/tutorial/lesson4/exercise/PixelMath.java:56 | method | exercise

public static int mean3x3(int[][] image, int centerX, int centerY) {

PixelMath.PixelMath

lesson_4/stubs/java/tutorial/lesson4/exercise/PixelMath.java:4 | constructor | stub

private PixelMath() {

PixelMath.clamp

lesson_4/stubs/java/tutorial/lesson4/exercise/PixelMath.java:7 | method | stub

public static int clamp(int value, int min, int max) {

PixelMath.invertGray

lesson_4/stubs/java/tutorial/lesson4/exercise/PixelMath.java:13 | method | stub

public static int invertGray(int gray) {

PixelMath.threshold

lesson_4/stubs/java/tutorial/lesson4/exercise/PixelMath.java:18 | method | stub

public static int threshold(int gray, int cutoff) {

PixelMath.luma

lesson_4/stubs/java/tutorial/lesson4/exercise/PixelMath.java:23 | method | stub

public static int luma(int rgb) {

PixelMath.packGray

lesson_4/stubs/java/tutorial/lesson4/exercise/PixelMath.java:29 | method | stub

public static int packGray(int gray) {

PixelMath.normalizeToByte

lesson_4/stubs/java/tutorial/lesson4/exercise/PixelMath.java:34 | method | stub

public static int normalizeToByte(double value, double min, double max) {

PixelMath.blend

lesson_4/stubs/java/tutorial/lesson4/exercise/PixelMath.java:40 | method | stub

public static int blend(int leftGray, int rightGray, double alpha) {

PixelMath.sobelMagnitude

lesson_4/stubs/java/tutorial/lesson4/exercise/PixelMath.java:46 | method | stub

public static int sobelMagnitude(int gx, int gy) {

PixelMath.sampleClamped

lesson_4/stubs/java/tutorial/lesson4/exercise/PixelMath.java:51 | method | stub

public static int sampleClamped(int[][] image, int x, int y) {

PixelMath.mean3x3

lesson_4/stubs/java/tutorial/lesson4/exercise/PixelMath.java:56 | method | stub

public static int mean3x3(int[][] image, int centerX, int centerY) {