Where It Actually Started: A Semester in the SoC Design Lab
Every DHM and ARMS paper I’ve written since started, in a very literal sense, in a small lab room on the fifth floor of the engineering building at Dong-A University — S06-RS834, home of the SoC Design Lab, Department of Electronics Engineering. I spent a semester there as an undergraduate under Korea’s research credit program (연구학점제) — coursework that earns academic credit for hands-on lab research instead of sitting in a lecture hall. Below are the actual weekly notes, kept close to their original structure rather than summarized down.
Week 1 — Digital Image Fundamentals
1. Screen Coordinate System
The coordinate system used in computer graphics and image processing differs structurally from the standard mathematical Cartesian system.
- Origin: the top-left corner of the screen is $(0, 0)$.
- x-axis: increases left → right.
- y-axis: increases top → bottom.
(0,0) ───────────────────────> X (increases left to right)
│
│
▼
Y (increases top to bottom)
When representing an image as a matrix, rows correspond to y and columns correspond to x — so image coordinates are indexed as $I(y, x)$, not $I(x, y)$.
5. Why this mattered later
This basic image math and representation later became the foundation for numerical reconstruction in Digital Holographic Microscopy and scattering-medium removal (dehazing) algorithms. Knowing the quantization characteristics of an image precisely — e.g. when designing Angular Spectrum Method propagation on a grid that satisfies the sampling theorem, or when controlling Poisson-Gaussian noise — turns out to be decisive for reducing modeling error.
Week 2 — C Programming Fundamentals
2. Basic Data Types
| Category | Type | Size | Range |
|---|---|---|---|
| Integer | short | 2 bytes | $-2^{15} \sim 2^{15}-1$ |
unsigned short | 2 bytes | $0 \sim 2^{16}-1$ | |
int | 4 bytes | $-2^{31} \sim 2^{31}-1$ | |
unsigned int | 4 bytes | $0 \sim 2^{32}-1$ | |
long int | 4 bytes | $-2^{31} \sim 2^{31}-1$ (typical 32/64-bit compiler) | |
unsigned long | 4 bytes | $0 \sim 2^{32}-1$ | |
| Float | float | 4 bytes | single precision, ≈ $\pm 3.4 \times 10^{38}$ |
double | 8 bytes | double precision, ≈ $\pm 1.79 \times 10^{308}$ | |
long double | 8 bytes | extended precision, ≈ $\pm 1.79 \times 10^{308}$ (12–16 bytes on some platforms) |
In C environments without a dedicated boolean type, logical/relational operations return integer
1(true) or0(false) into the control flow.
4. Control Flow
for loop’s three components — each with a distinct execution timing:
for (initialization; condition; increment) {
/* loop body */
}
- Initialization: runs exactly once, at loop start.
- Condition: evaluated before every iteration; loop exits the moment this is false.
- Increment: runs after each iteration’s body completes.
switch-case — a jump-table alternative to nested if-else:
graph TD
classDef dec fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px;
classDef proc fill:#e8f5e9,stroke:#4caf50,stroke-width:1px;
classDef startEnd fill:#ececff,stroke:#9370db,stroke-width:2px;
Start(["Switch Expression"]):::startEnd --> Case1{"case 1?"}:::dec
Case1 -- "Yes" --> Action1["Code 1"]:::proc
Action1 --> Break1{"break present?"}:::dec
Break1 -- "Yes" --> End(["Exit switch"]):::startEnd
Break1 -- "No (fall-through)" --> Action2
Case1 -- "No" --> Case2{"case 2?"}:::dec
Case2 -- "Yes" --> Action2["Code 2"]:::proc
Action2 --> Break2{"break present?"}:::dec
Break2 -- "Yes" --> End
Break2 -- "No" --> DefaultAction["default code"]:::proc
Case2 -- "No" --> DefaultAction
DefaultAction --> End
- If
breakis omitted, execution falls through into the nextcaseblock unconditionally — a common bug source. -
defaultplays the role of a trailingelse.
while vs. do-while — differ in when the condition is checked:
-
while(check-first): if the condition is false on entry, the body never runs. -
do-while(check-last): the body always runs at least once, condition is checked afterward. Requires a trailing;after the closingwhile(...).
Week 3 — C Memory & Systems
1. Pointers & Memory Access
- Pointer declaration (
*):int *p;declares “a variable that holds an address.” - Address-of (
&): obtains a variable’s physical address. - Array name decay: a bare array name is a constant pointer to element 0 —
int *p = arr;, never&arrfor this purpose. - Dereference (
*):*preads/writes the value at the addresspholds; printingpbare prints the address itself.
graph TD
classDef ptr fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px;
classDef val fill:#ffe6cc,stroke:#d79b00,stroke-width:2px;
classDef addr fill:#f5f5f5,stroke:#9e9e9e,stroke-width:1px;
subgraph Pointer ["Pointer Variable"]
P["int *p = arr;"]:::ptr
end
subgraph MemoryArray ["arr[5] Layout"]
A0["arr[0]=10"]:::val
A1["arr[1]=20"]:::val
A2["arr[2]=30"]:::val
A3["arr[3]=40"]:::val
A4["arr[4]=50"]:::val
end
subgraph PhysicalAddr ["Physical Addresses"]
Addr0["0x1000"]:::addr
Addr1["0x1004"]:::addr
Addr2["0x1008"]:::addr
Addr3["0x100C"]:::addr
Addr4["0x1010"]:::addr
end
P -- "Points to array start" --> Addr0
Addr0 --- A0
Addr1 --- A1
Addr2 --- A2
Addr3 --- A3
Addr4 --- A4
P -. "dereference *p" .-> A0
P -. "dereference *(p+1)" .-> A1
3. Dynamic Memory Allocation
[program start] ──> malloc/calloc ──> heap space allocated ──> free(p) (prevents dangling)
-
malloc(size)— grabs a contiguous heap block; contents are garbage, uninitialized. -
calloc(num, size)— same, but zero-clears every byte before returning. -
realloc(ptr, size)— resizes an existing block; existing data is preserved (copied to a new location if it can’t grow in place).
#include <stdio.h>
#include <stdlib.h>
int main() {
int i, hap = 0;
int *p = NULL;
int count;
printf("How many numbers to enter?: ");
if (scanf_s("%d", &count) != 1 || count <= 0) {
printf("Invalid input.\n");
return 1;
}
p = (int*)malloc(sizeof(int) * count);
if (p == NULL) {
printf("Dynamic memory allocation was refused.\n");
return 1;
}
for (i = 0; i < count; i++) {
printf("Enter element %d: ", i + 1);
scanf_s("%d", &(*(p + i)));
}
for (i = 0; i < count; i++) {
hap = hap + *(p + i);
}
printf("Sum of all entered elements: %d\n", hap);
free(p);
p = NULL; // prevent dangling pointer reuse
return 0;
}
5. Debugging Troubleshooting: Visual Studio PDB Loading Errors
A PDB (Program Database) file is the symbol database that 1:1-maps compiled machine binary back to human-readable source. When Visual Studio’s debugger (F5) launches, it tries to resolve Windows system DLL symbols (kernel32.dll, etc.) against Microsoft’s remote symbol server — and if that server is slow or unreachable, debug entry stalls or hangs entirely.
Fix: Debug → Options → Debugging → Symbols, and uncheck Microsoft Symbol Servers. This eliminates the network round-trip entirely and forces the debugger to rely only on local build symbols.
Faster daily-use tip: for pure logic checks that don’t need kernel-level stack tracing, Ctrl+F5 (Start Without Debugging) skips the symbol-loading path entirely and launches the built binary directly.
Week 4 — OpenCV Image Matrix Basics
1. cv::imread Loading Mode Analysis
cv::Mat imread(const cv::String& filename, int flags = cv::IMREAD_COLOR);
-
cv::IMREAD_COLOR(default, flag1) — force-loads as 3-channel BGR (24-bit), even if the source file is grayscale (duplicated across channels). -
cv::IMREAD_GRAYSCALE(flag0) — loads as 1-channel grayscale (8-bit); color sources are compressed via the internal conversion formula. -
cv::IMREAD_UNCHANGED(flag-1) — loads the file’s native format with no conversion; required for 4-channel BGRA sources with an alpha/transparency channel.
graph TD
classDef input fill:#ffffff,stroke:#333333,stroke-width:2px;
classDef op fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px;
classDef mem fill:#ffe6cc,stroke:#d79b00,stroke-width:2px;
classDef gray fill:#f5f5f5,stroke:#9e9e9e,stroke-width:2px;
InputFile["Input file (e.g. RGBA 4-channel source)"]:::input
subgraph ReadingOptions ["cv::imread Loading Flags"]
FlagColor["cv::IMREAD_COLOR (1)"]:::op
FlagGray["cv::IMREAD_GRAYSCALE (0)"]:::op
FlagUnchanged["cv::IMREAD_UNCHANGED (-1)"]:::op
end
subgraph MemoryLayout ["Memory Allocation (cv::Mat)"]
MatColor["cv::Mat (3-ch BGR) / [B][G][R] forced replication"]:::mem
MatGray["cv::Mat (1-ch Gray) / Y=0.299R+0.587G+0.114B"]:::gray
MatUnchanged["cv::Mat (4-ch BGRA) / original layout preserved"]:::mem
end
InputFile --> FlagColor --> MatColor
InputFile --> FlagGray --> MatGray
InputFile --> FlagUnchanged --> MatUnchanged
Week 5 — Neighbor-Pixel Convolution Design
1. 2D Spatial Convolution — Math
Spatial filtering multiplies a pixel and its neighbors by a filter kernel’s weights and sums the result. For a $(2M+1) \times (2M+1)$ kernel $K$ applied to image $I$:
\[O(y, x) = (I * K)(y, x) = \sum_{j=-M}^{M} \sum_{i=-M}^{M} I(y - j, x - i) \cdot K(j + M, i + M)\]This single operation underlies most low-level CV: edge detection (Sobel/Laplacian), Gaussian blur/denoising, sharpening.
graph TD
classDef edge fill:#ffebee,stroke:#c62828,stroke-width:2px;
classDef valid fill:#e8f5e9,stroke:#4caf50,stroke-width:2px;
classDef mask fill:#dae8fc,stroke:#6c8ebf,stroke-width:2px;
subgraph OutOfBounds ["Out of Bounds"]
O11["?"]:::edge
O12["?"]:::edge
O13["?"]:::edge
O21["?"]:::edge
O31["?"]:::edge
end
subgraph InBounds ["Valid Image Pixels"]
P22["I(0,0)"]:::valid
P23["I(0,1)"]:::valid
P32["I(1,0)"]:::valid
P33["I(1,1)"]:::valid
end
subgraph Mask3x3 ["3x3 Mask"]
M11["K(-1,-1)"]:::mask
M12["K(-1,0)"]:::mask
M13["K(-1,1)"]:::mask
M21["K(0,-1)"]:::mask
M22["K(0,0) Anchor"]:::mask
M23["K(0,1)"]:::mask
M31["K(1,-1)"]:::mask
M32["K(1,0)"]:::mask
M33["K(1,1)"]:::mask
end
M22 --> |center align| P22
M11 -.-> |out-of-bound error| O11
M12 -.-> O12
M13 -.-> O13
M21 -.-> O21
M31 -.-> O31
M23 --> P23
M32 --> P32
M33 --> P33
2. Zero Padding for Boundary Handling
Border pixels can’t have a full neighborhood — the kernel would read past the image edge. Zero padding surrounds the image with a margin of zero-valued pixels so every pixel, including original border pixels, gets a complete neighborhood.
For a $W \times H$ image padded by margin $M$, the padded matrix is $(W+2M) \times (H+2M)$, and original coordinate $(y,x)$ maps to padded coordinate $(y+M, x+M)$:
Padding Margin = 1
(0,0) -> (1,1)
┌───┬───┬───┬───┬───┐
│ 0 │ 0 │ 0 │ 0 │ 0 │ <- padding margin (y=0)
├───┼───┼───┼───┼───┤
│ 0 │ P │ P │ P │ 0 │ <- valid data region (y=1)
├───┼───┼───┼───┼───┤
│ 0 │ P │ P │ P │ 0 │ (P = original pixels)
└───┴───┴───┴───┴───┘
Week 6 — Spatial Filtering, Kernel Design & the ROI Bug
The bug that actually taught me something
Implementing a 5×5 filter, I wrote this:
cv::Mat dst(SubM.rows, SubM.cols, CV_8UC1);
for (int y = 0; y < SubM.rows; ++y) {
const uchar* prevRow2 = SubM.ptr<uchar>(y - 2); // segfaults here
const uchar* prevRow1 = SubM.ptr<uchar>(y - 1);
const uchar* currRow = SubM.ptr<uchar>(y);
// ... 5x5 filter math
}
The reasoning felt airtight at the time: SubM is an ROI sub-matrix sharing its physical heap buffer with the parent cv::Mat — no copy, just a view. So walking the pointer backward past the ROI’s own boundary with a negative row index should land on the parent’s actual neighboring pixels.
It wasn’t airtight, for two reasons I didn’t know yet:
-
Mat::ptr<T>(y)runs an internalCV_DbgAssert(y >= 0 && y < rows)bounds check — fires immediately in debug builds. - In release builds where the assert compiles out, the negative offset does walk into the parent’s physical memory — but the sub-matrix’s row stride doesn’t match the parent’s stride once the ROI’s column offset is accounted for. Naive pointer subtraction across that mismatch lands on a diagonally-wrong address, not “the row above.” Result: streak artifacts at frame boundaries, or a walk straight past
datastartinto OS-protected memory → segfault.
Fix — extend the ROI’s officially addressable region first, via adjustROI, instead of hand-rolling pointer arithmetic:
cv::Mat tempSub = SubM.clone(); // preserve the original view
tempSub.adjustROI(2, 2, 2, 2); // expand the header 2px each direction
for (int y = 0; y < SubM.rows; ++y) {
const uchar* srcRow_m2 = tempSub.ptr<uchar>(y);
const uchar* srcRow_m1 = tempSub.ptr<uchar>(y + 1);
const uchar* srcRow_0 = tempSub.ptr<uchar>(y + 2);
// now safe and correct
}
adjustROI doesn’t move any data — it renegotiates the matrix header’s addressable window against the parent buffer’s real bounds. The gap between my first attempt and this one is the gap between “I know this is a shallow-copy view” and “I understand exactly what geometry that view’s header is and isn’t allowed to promise me.”
3. High-Performance Pixel-Pointer 2D Sliding Window (C++)
Projecting the SoC Design Lab’s cache-efficiency and HW-SW co-design lens onto this: skip Mat::at()’s repeated 2D address math and cache the row pointer once per kernel row instead.
cv::Mat apply5x5FilterFast(const cv::Mat& src, const float kernel[5][5], float denom) {
CV_Assert(src.type() == CV_8UC1);
int H = src.rows, W = src.cols;
cv::Mat dst(H, W, CV_8UC1, cv::Scalar(0));
const int radius = 2;
for (int y = radius; y < H - radius; ++y) {
uchar* dstRow = dst.ptr<uchar>(y);
for (int x = radius; x < W - radius; ++x) {
float sum = 0.0f;
for (int ky = -radius; ky <= radius; ++ky) {
const uchar* srcRow = src.ptr<uchar>(y + ky); // fetched once per kernel row
sum += srcRow[x - 2] * kernel[ky + 2][0]
+ srcRow[x - 1] * kernel[ky + 2][1]
+ srcRow[x ] * kernel[ky + 2][2]
+ srcRow[x + 1] * kernel[ky + 2][3]
+ srcRow[x + 2] * kernel[ky + 2][4];
}
dstRow[x] = cv::saturate_cast<uchar>(sum / denom);
}
}
return dst;
}
int main() {
float gaussian5x5[5][5] = {
{2, 4, 5, 4, 2},
{4, 9, 12, 9, 4},
{5, 12, 15, 12, 5},
{4, 9, 12, 9, 4},
{2, 4, 5, 4, 2}
};
float denom = 159.0f;
cv::Mat dummyImg(100, 100, CV_8UC1, cv::Scalar(100));
cv::Mat result = apply5x5FilterFast(dummyImg, gaussian5x5, denom);
std::cout << "Low-level pointer-based 5x5 Gaussian spatial filter complete." << std::endl;
return 0;
}
Why this is fast:
-
src.ptr<uchar>(y + ky)is hoisted out of the innermost loop — loaded once per kernel row instead of recomputed as a 2D address on every tap, maximizing spatial/temporal cache locality. -
srcRow[x-2]…srcRow[x+2]are contiguous memory references that compile down to register-offset addressing, giving the compiler room for instruction-level parallelism.
Week 7 — cv::Mat Physical Structure
1. Dual Physical Structure (Header vs. Data Pointer)
cv::Mat splits into a small, fixed-size header (dims, type/step, refcount pointer, data pointer) and a separately-allocated heap data buffer.
graph TD
classDef header fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px;
classDef buffer fill:#ffe6cc,stroke:#d79b00,stroke-width:2px;
classDef counter fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px;
classDef main fill:#f3e5f5,stroke:#8e24aa,stroke-width:2px;
subgraph MatObject ["Stack Header Area"]
M["cv::Mat object"]:::main
H_Dims["rows / cols / dims"]:::header
H_Type["type / step"]:::header
H_Ref["refcount pointer"]:::header
H_Data["data pointer"]:::header
M --> H_Dims
M --> H_Type
M --> H_Ref
M --> H_Data
end
subgraph HeapArea ["Heap Memory Area"]
U["UMatData shared refcount"]:::counter
D["Physical data buffer B0 G0 R0 B1 G1 R1 ..."]:::buffer
end
H_Ref -- "manages memory lifetime" --> U
H_Data -- "points to actual data" --> D
- Header: fixed byte size; copying it is $O(1)$.
- Data buffer: the large heap region pixels actually live in.
- This split is why cropping/ROI is instant: only a new header is built, with no pixel data copy.
2. Reference Counting — Smart Memory Lifetime
cv::Mat embeds a shared_ptr-like reference-counting mechanism to prevent leaks and sync issues.
- Assignment / copy constructor (
Mat A = B;orMat A(B);) — shallow copy, $O(1)$: copies only the header, shares the data pointer, incrementsrefcount. -
clone()/copyTo()— deep copy, $O(N)$: allocates new heap memory andmemcpys the pixel data; fully independent lifecycle from that point.
cv::Mat A = cv::imread("image.png");
cv::Mat B = A; // shallow copy: shared data, refcount = 2
cv::Mat C = A.clone(); // deep copy: new buffer, refcount = 1 (independent)
graph TD
classDef original fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px;
classDef shallow fill:#f3e5f5,stroke:#8e24aa,stroke-width:2px;
classDef deep fill:#ffe6cc,stroke:#d79b00,stroke-width:2px;
classDef shared fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px;
subgraph Stack ["Stack Area"]
A["Mat A (original)"]:::original
B["Mat B (shallow copy)"]:::shallow
C["Mat C (deep copy)"]:::deep
end
subgraph Heap1 ["Heap Area A"]
Ref1["UMatData refcount=2"]:::shared
Data1["shared pixel buffer"]:::shared
end
subgraph Heap2 ["Heap Area B"]
Ref2["UMatData refcount=1"]:::deep
Data2["independent pixel buffer"]:::deep
end
A -- "header copy" --> B
A -- "data pointer" --> Data1
B -- "data pointer" --> Data1
A -- "refcount address" --> Ref1
B -- "refcount address" --> Ref1
C -. "deep copy" .-> Heap2
C -- "data pointer" --> Data2
C -- "refcount address" --> Ref2
Data1 -- "memcpy" --> Data2
When a Mat goes out of scope, its destructor decrements refcount; only when that count hits zero does MatAllocator actually deallocate the heap buffer.
4. Physical step Offset Formula
\[\text{addr}(M_{i,j}) = M.\text{data} + M.\text{step}[0] \cdot i + M.\text{step}[1] \cdot j\] -
M.data— physical start address. -
M.step[0](row step) — bytes to skip to move down one row ($W \times \text{channels} \times \text{bytes/channel}$, plus alignment padding). -
M.step[1](column/element step) — bytes to skip to move one column over.
Directly manipulating step bytes is what lets discontinuous sub-matrices (ROIs) still get pure-pointer-arithmetic, zero-overhead access to any pixel.
Week 13 — Color Space Pointer Manipulation
1. BGR Interleaved Memory Layout
OpenCV stores color images as BGR, not RGB — a historical artifact of Windows DIB compatibility — and channels are interleaved, not stored as separate planes: B,G,R,B,G,R,... in one contiguous run.
graph TD
classDef blueChannel fill:#e3f2fd,stroke:#1e88e5,stroke-width:2px;
classDef greenChannel fill:#e8f5e9,stroke:#43a047,stroke-width:2px;
classDef redChannel fill:#ffebee,stroke:#e53935,stroke-width:2px;
classDef pointer fill:#fff3e0,stroke:#fb8c00,stroke-width:2px;
subgraph PixelArray ["BGR Interleaved Memory Layout (1D Byte Array)"]
B0["B(0)"]:::blueChannel
G0["G(1)"]:::greenChannel
R0["R(2)"]:::redChannel
B1["B(3)"]:::blueChannel
G1["G(4)"]:::greenChannel
R1["R(5)"]:::redChannel
B2["B(6)"]:::blueChannel
G2["G(7)"]:::greenChannel
R2["R(8)"]:::redChannel
B0 --- G0 --- R0 --- B1 --- G1 --- R1 --- B2 --- G2 --- R2
end
subgraph Offsets ["Pixel Index & Byte Offsets"]
Ptr0["Pixel 0: (y*W+0)*3"]:::pointer
Ptr1["Pixel 1: (y*W+1)*3"]:::pointer
Ptr2["Pixel 2: (y*W+2)*3"]:::pointer
end
Ptr0 -.-> B0
Ptr1 -.-> B1
Ptr2 -.-> B2
- Grayscale offset (1-channel): $\text{Offset} = y \times W + x$
- BGR offset (3-channel): $\text{Offset} = (y \times W + x) \times 3$ — lands on Blue; Green is
+1, Red is+2.
2. Grayscale Intensity — Biological & Mathematical Basis
Naive averaging ($\frac{R+G+B}{3}$) doesn’t match human perceived brightness. Retinal cone cells are most sensitive to green, then red, then blue — weighted accordingly:
\[Y = 0.299R + 0.587G + 0.114B\]This is the actual formula cv::cvtColor(src, dst, cv::COLOR_BGR2GRAY) runs internally, as an optimized fixed-point computation.
4. BGR Pointer Address Arithmetic
A 2D Mat is physically a flattened 1D buffer. The blue-channel address of $(y,x)$ — &src.data[y*step[0] + x*step[1] + 0] — can have its row-jump term (y*step[0]) hoisted out of the inner loop via ptr<uchar>(y), leaving only $O(1)$ pointer increments (3*x) inside the loop, maximizing memory bandwidth utilization.
Why this ended up mattering for optics
None of this is computational optics. But every DHM paper I’ve written since involves windowing sidebands in a Fourier-transformed matrix, and every ARMS-line paper involves per-block frequency-domain masks over overlapping image regions — every single one of them is, underneath the physics, a program that has to slice sub-regions out of a large 2D buffer correctly and fast. The SoC Design Lab semester is where “correctly and fast” stopped being something I trusted a library to handle for me and became something I could reason about at the memory-layout level.
It’s also, not coincidentally, the first stage of what our lab now half-jokingly calls the growth path: SoC Design Lab (systems mastery) → KAIST microdegree tracks (mathematical depth) → Master’s in the 3D Optical Imaging Systems Lab (DHM, ARMS) → PhD-track computational imaging. Each stage’s “why” only became obvious from the next one.
Affiliation: SoC Design Lab, Department of Electronics Engineering, Dong-A University, under the undergraduate research credit program (연구학점제).
Enjoy Reading This Article?
Here are some more articles you might like to read next: