Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
with:
args: >
-Dsonar.cfamily.build-wrapper-output=bw-output
-Dsonar.cfamily.compile-commands=bw-output/compile_commands.json
-Dsonar.coverageReportPaths=coverage.xml

build-web:
Expand Down Expand Up @@ -78,6 +78,12 @@ jobs:
needs: build-web
# Only deploy on pushes to main, not PRs
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
permissions:
deployments: write
contents: read
environment:
name: production
url: ${{ steps.deploy.outputs.deployment_url }}
steps:
- uses: actions/checkout@v4

Expand All @@ -97,7 +103,10 @@ jobs:
run: vercel build --prod --token=$VERCEL_TOKEN

- name: Deploy Project Artifacts to Vercel
run: vercel deploy --prebuilt --prod --token=$VERCEL_TOKEN
id: deploy
run: |
DEPLOY_URL=$(vercel deploy --prebuilt --prod --token=$VERCEL_TOKEN --yes)
echo "deployment_url=$DEPLOY_URL" >> $GITHUB_OUTPUT
env:
VERCEL_ORG_ID: ${{ secrets.ORGID }}
VERCEL_PROJECT_ID: ${{ secrets.PROJECTID }}
Expand Down
10 changes: 10 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ if(ENABLE_COVERAGE)
endif()
endif()

# --- Sanitizers ---
option(ENABLE_SANITIZERS "Enable ASan and UBSan" OFF)
if(ENABLE_SANITIZERS)
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang")
message(STATUS "Enabling AddressSanitizer and UndefinedBehaviorSanitizer")
add_compile_options(-fsanitize=address,undefined -fno-omit-frame-pointer)
add_link_options(-fsanitize=address,undefined)
endif()
endif()

# --- Subdirectories ---
add_subdirectory(core)
add_subdirectory(apps/native)
10 changes: 9 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ WEB_COMPILER = emcc
WEB_FLAGS = -Icore/inc -O3 \
-s WASM=1 \
-s ALLOW_MEMORY_GROWTH=1 \
-s EXPORTED_FUNCTIONS='["_init_session", "_process_image", "_get_view_ptr", "_set_view_tint", "_get_psnr_y", "_get_psnr_cr", "_get_psnr_cb", "_get_ssim_y", "_get_ssim_cr", "_get_ssim_cb", "_malloc", "_free"]' \
-s EXPORTED_FUNCTIONS='["_init_session", "_process_image", "_get_view_ptr", "_set_view_tint", "_get_psnr_y", "_get_psnr_cr", "_get_psnr_cb", "_get_ssim_y", "_get_ssim_cr", "_get_ssim_cb", "_inspect_block_data", "_malloc", "_free"]' \
-s EXPORTED_RUNTIME_METHODS='["cwrap", "ccall", "HEAPU8"]'

# Source: Core C++ + Web Glue C++ (in src folder)
Expand Down Expand Up @@ -70,6 +70,14 @@ coverage:
# In gcovr 8.6, we use the search path at the end instead of --build-root
gcovr --sonarqube -o coverage.xml -r . --filter core/src/ -e build/ --gcov-ignore-parse-errors=all build

# 6. Sanitize (ASan + UBSan)
sanitize:
@echo "🛡️ Running with Sanitizers..."
@mkdir -p build
@if [ -d build/CMakeCache.txt ]; then rm build/CMakeCache.txt; fi
@cd build && cmake .. -DENABLE_SANITIZERS=ON && make -j$(NPROCS)
@cd build && ctest --output-on-failure

# ------------------------------------
# 🧹 CLEANUP
# ------------------------------------
Expand Down
114 changes: 114 additions & 0 deletions apps/native/CodecExplorerApp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ CodecExplorerApp::CodecExplorerApp(const std::string& imagePath, ImageCodec::Chr
// Per OpenCV warning, use nullptr for the value pointer and set the position manually.
cv::createTrackbar("Quality", m_state.windowName, nullptr, 100, onQualityChangeStatic, this);
cv::setTrackbarPos("Quality", m_state.windowName, m_quality);
cv::setMouseCallback(m_state.windowName, onMouseStatic, this);
}

void CodecExplorerApp::run() {
Expand Down Expand Up @@ -105,7 +106,9 @@ void CodecExplorerApp::handleKey(int key) {
else if (key == 't') { m_useTint = !m_useTint; viewChanged = true; }
else if (key == '4') { m_chromaSubsampling = ImageCodec::ChromaSubsampling::CS_444; codecChanged = true; }
else if (key == '2') { m_chromaSubsampling = ImageCodec::ChromaSubsampling::CS_422; codecChanged = true; }
else if (key == '2') { m_chromaSubsampling = ImageCodec::ChromaSubsampling::CS_422; codecChanged = true; }
else if (key == '0') { m_chromaSubsampling = ImageCodec::ChromaSubsampling::CS_420; codecChanged = true; }
else if (key == 'c') { m_showInspection = false; viewChanged = true; }


if (codecChanged) {
Expand All @@ -126,6 +129,72 @@ void CodecExplorerApp::onQualityChangeStatic(int quality, void* userdata) {
void CodecExplorerApp::onQualityChange(int quality) {
m_quality = std::max(1, quality);
updateCodecOutput();
updateCodecOutput();
render();
}

void CodecExplorerApp::onMouseStatic(int event, int x, int y, int flags, void* userdata) {
auto* app = static_cast<CodecExplorerApp*>(userdata);
if (app) {
app->onMouse(event, x, y, flags);
}
}

void CodecExplorerApp::onMouse(int event, int x, int y, int flags) {
if (event == cv::EVENT_LBUTTONDOWN) {
// Check if click is within the original image area (top-left)
if (x < m_state.originalImage.width() && y < m_state.originalImage.height()) {
inspectBlockAt(x, y);
} else {
// Hide inspection if clicked elsewhere
m_showInspection = false;
render();
}
}
}

void CodecExplorerApp::inspectBlockAt(int x, int y) {
m_selectedBlockX = x / 8;
m_selectedBlockY = y / 8;
m_showInspection = true;

ImageCodec codec(m_quality, true, m_chromaSubsampling);

// Determine which channel we are inspecting based on view mode
if (m_state.mode == AppState::ViewMode::Cr) {
// Need to pass the Cr channel. BUT wait, inspectBlock expects the input channel.
// We need to extract the channel from the ORIGINAL image first, as per logic
// But `inspectBlock` does internally convert if we passed the whole image? No.
// `inspectBlock` takes a channel Image.

// We need the original YCrCb image to pass the correct channel
Image ycrcb = bgrToYCrCb(m_state.originalImage);
Image cr(ycrcb.width(), ycrcb.height(), 1);
const double* src = ycrcb.data();
double* dst = cr.data();
for(size_t i=0; i<ycrcb.width()*ycrcb.height(); ++i) dst[i] = src[i*3 + 1];

m_inspectionData = codec.inspectBlock(cr, m_selectedBlockX, m_selectedBlockY, true);

} else if (m_state.mode == AppState::ViewMode::Cb) {
Image ycrcb = bgrToYCrCb(m_state.originalImage);
Image cb(ycrcb.width(), ycrcb.height(), 1);
const double* src = ycrcb.data();
double* dst = cb.data();
for(size_t i=0; i<ycrcb.width()*ycrcb.height(); ++i) dst[i] = src[i*3 + 2];

m_inspectionData = codec.inspectBlock(cb, m_selectedBlockX, m_selectedBlockY, true);
} else {
// Default to Y for RGB, Artifacts, or Y mode
Image ycrcb = bgrToYCrCb(m_state.originalImage);
Image y(ycrcb.width(), ycrcb.height(), 1);
const double* src = ycrcb.data();
double* dst = y.data();
for(size_t i=0; i<ycrcb.width()*ycrcb.height(); ++i) dst[i] = src[i*3 + 0];

m_inspectionData = codec.inspectBlock(y, m_selectedBlockX, m_selectedBlockY, false);
}

render();
}

Expand Down Expand Up @@ -225,4 +294,49 @@ void CodecExplorerApp::render() {
drawMetricsDashboard(viewWithFooter, dashboardX, yBase, m_state.metrics);

cv::imshow(m_state.windowName, viewWithFooter);

if (m_showInspection) {
// Create an overlay or a separate window for block details
// Increased size to prevent overlap
cv::Mat inspectionView(600, 800, CV_8UC3, cv::Scalar(30,30,30));

// Add footer instruction
cv::putText(inspectionView, "Press 'c' to close", {10, 580}, cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(150,150,150), 1);

auto drawGrid = [&](int offsetX, int offsetY, const char* title, double data[8][8], bool isInt) {
cv::putText(inspectionView, title, {offsetX, offsetY - 10}, cv::FONT_HERSHEY_SIMPLEX, 0.6, cv::Scalar(200,200,200), 1);
for(int i=0; i<8; ++i) {
for(int j=0; j<8; ++j) {
std::string valStr;
if (isInt) {
valStr = std::to_string((int)std::round(data[i][j]));
} else {
std::ostringstream oss;
oss << std::fixed << std::setprecision(1) << data[i][j];
valStr = oss.str();
}

cv::Scalar color(255,255,255);
if (data[i][j] == 0) color = cv::Scalar(100,100,100);

// Increased spacing: 40 -> 50 for X, 20 -> 25 for Y
int px = offsetX + j * 45;
int py = offsetY + i * 25;
cv::putText(inspectionView, valStr, {px, py}, cv::FONT_HERSHEY_SIMPLEX, 0.35, color, 1);
}
}
};

// Adjusted positions
drawGrid(20, 50, "Original", m_inspectionData.original, true);
drawGrid(420, 50, "DCT", m_inspectionData.dct, false);
drawGrid(20, 300, "Quantized", m_inspectionData.quantized, true);
drawGrid(420, 300, "Reconstructed", m_inspectionData.reconstructed, true);

cv::imshow("Block Inspection", inspectionView);
} else {
try {
cv::destroyWindow("Block Inspection");
} catch(...) {}
}
}
11 changes: 11 additions & 0 deletions apps/native/CodecExplorerApp.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,19 @@ class CodecExplorerApp {
static void onQualityChangeStatic(int quality, void* userdata);
void onQualityChange(int quality);

// Mouse Handling
static void onMouseStatic(int event, int x, int y, int flags, void* userdata);
void onMouse(int event, int x, int y, int flags);
void inspectBlockAt(int x, int y);

AppState m_state;
int m_quality = 50;
ImageCodec::ChromaSubsampling m_chromaSubsampling;
bool m_useTint = true;

// Inspection State
bool m_showInspection = false;
int m_selectedBlockX = 0;
int m_selectedBlockY = 0;
ImageCodec::BlockDebugData m_inspectionData;
};
11 changes: 11 additions & 0 deletions core/inc/ImageCodec.h
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,17 @@ class ImageCodec {
const double quantTable[8][8]);
Image downsampleChannel(const Image& channel, ChromaSubsampling cs) const;
Image upsampleChannel(const Image& channel, int targetWidth, int targetHeight, ChromaSubsampling cs) const;

public:
struct BlockDebugData {
double original[8][8];
double dct[8][8];
double quantTable[8][8];
double quantized[8][8];
double reconstructed[8][8];
};

BlockDebugData inspectBlock(const Image& channel, int blockX, int blockY, bool isChroma = false);
};

#endif
82 changes: 82 additions & 0 deletions core/src/ImageCodec.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -303,4 +303,86 @@ Image ImageCodec::process(const Image& bgrImage)
}

return ycrcbToBgr(merged);
}

ImageCodec::BlockDebugData ImageCodec::inspectBlock(const Image& channel, int blockX, int blockY, bool isChroma) {
BlockDebugData data;
const double (*quantTable)[8] = isChroma ? m_chromaQuantTable : m_lumaQuantTable;

// 1. Copy Quantization Table
for(int i=0; i<8; ++i) {
for(int j=0; j<8; ++j) {
data.quantTable[i][j] = quantTable[i][j];
}
}

// 2. Extract Original Block
// Ensure we don't go out of bounds
int startX = blockX * 8;
int startY = blockY * 8;

// Fill with zeros first
for(int i=0; i<8; ++i)
for(int j=0; j<8; ++j)
data.original[i][j] = 0.0;

const double* channelData = channel.data();
int width = channel.width();
int height = channel.height();

for(int i=0; i<8; ++i) {
for(int j=0; j<8; ++j) {
int y = startY + i;
int x = startX + j;
if (x < width && y < height) {
data.original[i][j] = channelData[y * width + x];
}
}
}

// 3. DCT
double blockCentered[8][8];
for(int i=0; i<8; ++i)
for(int j=0; j<8; ++j)
blockCentered[i][j] = data.original[i][j] - 128.0;

dct8x8(blockCentered, data.dct);

// 4. Quantization
if (m_enableQuantization) {
for(int i=0; i<8; ++i) {
for(int j=0; j<8; ++j) {
double coeff = data.dct[i][j] / quantTable[i][j];
data.quantized[i][j] = std::round(coeff); // Store integer index
// But for display in "Quantized" view, we might want the actual integer value
// In processChannel we do: dctBlock[i][j] = std::round(coeff) * quantTable[i][j];
// effectively dequantizing immediately.
// Let's store the quantized integer index in `quantized`
}
}
} else {
for(int i=0; i<8; ++i)
for(int j=0; j<8; ++j)
data.quantized[i][j] = data.dct[i][j];
}

// 5. Dequantization & IDCT (Reconstruction)
double dequantized[8][8];
for(int i=0; i<8; ++i) {
for(int j=0; j<8; ++j) {
if (m_enableQuantization)
dequantized[i][j] = data.quantized[i][j] * quantTable[i][j];
else
dequantized[i][j] = data.quantized[i][j];
}
}

double reconBlock[8][8];
idct8x8(dequantized, reconBlock);

for(int i=0; i<8; ++i)
for(int j=0; j<8; ++j)
data.reconstructed[i][j] = reconBlock[i][j] + 128.0;

return data;
}
2 changes: 2 additions & 0 deletions core/tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ add_executable(codec_core_tests
image_test.cpp
colorspace_test.cpp
transform_test.cpp
test_imagecodec.cpp
test_codecanalysis.cpp
)

target_link_libraries(codec_core_tests
Expand Down
Loading