├── decoder ├── img │ ├── output.png │ └── screenshot-with-blind-water-mark.png ├── CMakeLists.txt └── src │ └── decoder.cpp ├── watermark ├── CMakeLists.txt └── src │ └── main.cpp └── readme.md /decoder/img/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leeeo2/blind-watermark/HEAD/decoder/img/output.png -------------------------------------------------------------------------------- /decoder/img/screenshot-with-blind-water-mark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leeeo2/blind-watermark/HEAD/decoder/img/screenshot-with-blind-water-mark.png -------------------------------------------------------------------------------- /watermark/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10.0) 2 | project(blind_water_mark) 3 | 4 | set(CMAKE_C_STANDARD 11) 5 | set(CMAKE_CXX_STANDARD 11) 6 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 7 | 8 | add_executable(${PROJECT_NAME} ./src/main.cpp) -------------------------------------------------------------------------------- /decoder/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10.0) 2 | project(blind_watermark_decoder) 3 | 4 | set(CMAKE_C_STANDARD 11) 5 | set(CMAKE_CXX_STANDARD 11) 6 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 7 | 8 | if (WIN32) 9 | set(OpenCV_DIR C:/opencv4/opencv-mingw/build) 10 | find_package(OpenCV REQUIRED) 11 | else() 12 | find_package(opencv REQUIRED) 13 | endif() 14 | 15 | 16 | 17 | include_directories(${OpenCV_INCLUDE_DIRS}) 18 | 19 | add_executable(${PROJECT_NAME} ./src/decoder.cpp) 20 | target_link_libraries(${PROJECT_NAME} ${OpenCV_LIBS}) -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 | # 简介 3 | 这是一种实时隐蔽水印(类似于盲水印)实现方案,可以用于云桌面和远程桌面等场景,在敏感信息被截图外流之后,通过截图解析可以得到水印,用于溯源。 4 | 5 | 与明水印相比,该方案实现的水印肉眼不可见,有效降低对正常操作的干扰。 6 | 7 | 与基于频域变换的实现方法相比,该方法使用与明水印相同的线性混合来添加水印,对计算资源消耗极底,几乎不消耗CPU资源,所以可做到实时添加。 8 | 9 | 仅实现了Windows操作系统下的叠加水印,其他平台可参考实现。 10 | 11 | 解析效果良好,但是在某些场景还是有局限性。 12 | 13 | # Description 14 | This is a real-time concealed watermark (similar to blind watermark) implementation scheme, which can be used in scenarios such as cloud desktop and remote desktop. After the inscription information is leaked out of the screenshot, the watermark can be obtained through screenshot analysis for traceability. 15 | 16 | Compared with the clear watermark, the watermark realized by this scheme is invisible to the naked eye, which effectively reduces the interference to the normal operation. 17 | 18 | Compared with the implementation method based on spatial transformation, this method uses the same linear mixing as the clear watermark to add watermarks, which consumes extremely low computing resources and hardly consumes CPU resources, so it can be added in real time. 19 | 20 | Only the superimposed watermark under the Windows operating system is implemented, and other platforms can refer to the implementation. 21 | 22 | The parsing effect is good, but there are still limitations in some scenarios. 23 | 24 | - 添加水印之后的截图 (source image) 25 | ![原图](./decoder/img/screenshot-with-blind-water-mark.png) 26 | 27 | - 解码结果 (decoded) 28 | ![img](./decoder/img/output.png) -------------------------------------------------------------------------------- /decoder/src/decoder.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #define CHAR_WIDTH 21 4 | using namespace cv; 5 | 6 | Vec3b markColor = {0, 0, 255}; 7 | bool accept(Vec3b temp) 8 | { 9 | int filter = 5; 10 | int count = 0; 11 | for (int i = 0; i < 3; i++) 12 | { 13 | if ((temp[i] > 0 && temp[i] <= filter)) 14 | { 15 | count++; 16 | } 17 | } 18 | return (count == 3); 19 | } 20 | 21 | Mat decode(Mat src) 22 | { 23 | Vec3b last = src.at(0, 0); 24 | int count = 0; 25 | for (int i = 0; i < src.rows; i++) 26 | { 27 | for (int j = 0; j < src.cols; j++) 28 | { 29 | Vec3b now = src.at(i, j); 30 | Vec3b temp = now - last; 31 | if (accept(temp)) 32 | { 33 | count++; 34 | } 35 | else 36 | { 37 | if (count > 0 && count <= CHAR_WIDTH) 38 | { 39 | for (int k = j - count; k < j; k++) 40 | { 41 | src.at(i, k) = markColor; 42 | } 43 | } 44 | count = 0; 45 | last = now; 46 | } 47 | } 48 | } 49 | count = 0; 50 | for (int i = src.rows - 1; i >= 0; i--) 51 | { 52 | for (int j = src.cols - 1; j >= 0; j--) 53 | { 54 | Vec3b now = src.at(i, j); 55 | Vec3b temp = now - last; 56 | if (accept(temp)) 57 | { 58 | count++; 59 | } 60 | else 61 | { 62 | if (count > 0 && count <= CHAR_WIDTH) 63 | { 64 | for (int k = j + count; k > j; k--) 65 | { 66 | src.at(i, k) = markColor; 67 | } 68 | } 69 | count = 0; 70 | last = now; 71 | } 72 | } 73 | } 74 | return src; 75 | } 76 | 77 | int main(int argc, char **argv) 78 | { 79 | if (argc != 3) 80 | { 81 | printf(" require 2 argument...\n"); 82 | printf(" Usage: %s [input] [output]\n", argv[0]); 83 | return -1; 84 | } 85 | 86 | std::string inputFileName = argv[1]; 87 | std::string outputFileName = argv[2]; 88 | 89 | Mat src = imread(inputFileName); 90 | if (!src.empty()) 91 | { 92 | imwrite(outputFileName, decode(src)); 93 | return 0; 94 | } 95 | return 0; 96 | } 97 | -------------------------------------------------------------------------------- /watermark/src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #define GLOBAL_MUTEX_STRING "watermark_process_mutex" 9 | #define BACKGROUND_COLOR 0xff000000 10 | #define DEFAULT_TRANSPARENCY 2 // 0-255 11 | #define DEFAULT_ROTATE_ANGLE 15 // 0-90 12 | #define DEFAULT_FONT_SIZE 20 13 | #define DEFAULT_FONT_COLOR 0x00ffffff 14 | #define INIT_ONE_MARK_WIDTH 320 15 | #define ONE_LINE_HEIGHT 21 // text height 16 | 17 | #define MATH_PI 3.1415926 //π 18 | #define MAX_BUFFER_LEN 1024 19 | #define AUTO_REFRESH_INTERVAL 1000 * 30 // 30s 20 | #define SET_TOP_INTERVAL 1000 // 1s 21 | #define TIMER_1 1 22 | #define TIMER_2 2 23 | 24 | typedef struct Geometry 25 | { 26 | uint32_t x; 27 | uint32_t y; 28 | uint32_t w; 29 | uint32_t h; 30 | std::string print() 31 | { 32 | std::stringstream s; 33 | s << "[" << x << "," << y << "," << w << "," << h << "]"; 34 | return s.str(); 35 | } 36 | } Geometry; 37 | 38 | typedef struct WaterMarkConfig 39 | { 40 | bool isInvisible; 41 | Geometry wndGeometry; 42 | uint32_t fontColor; // 0x0 ~ 0xffffffff 43 | uint32_t fontSize; 44 | uint32_t transparency; // 0~255 45 | uint32_t angle; // 0~90 46 | bool showComputerName; 47 | bool showLocalTime; 48 | std::string customText; 49 | } WaterMarkConfig; 50 | 51 | Geometry GetDesktopGeometry() 52 | { 53 | int x = ::GetSystemMetrics(SM_XVIRTUALSCREEN); 54 | int y = ::GetSystemMetrics(SM_YVIRTUALSCREEN); 55 | int w = ::GetSystemMetrics(SM_CXVIRTUALSCREEN); 56 | int h = ::GetSystemMetrics(SM_CYVIRTUALSCREEN); 57 | return {x, y, w, h}; 58 | } 59 | 60 | WaterMarkConfig g_waterMarkconf = {false, GetDesktopGeometry(), DEFAULT_FONT_COLOR, DEFAULT_FONT_SIZE, DEFAULT_TRANSPARENCY, DEFAULT_ROTATE_ANGLE, true, true, "This is a watermark."}; 61 | 62 | std::string GetComputerNameStr() 63 | { 64 | DWORD bufLen = MAX_BUFFER_LEN; 65 | CHAR buf[MAX_BUFFER_LEN] = {0}; 66 | if (!GetComputerNameA(buf, &bufLen)) 67 | { 68 | std::cout << "get computer name failed,err code:" << GetLastError() << std::endl; 69 | return std::string("unknown"); 70 | } 71 | return std::string(buf); 72 | } 73 | 74 | std::string GetLocaltimeStr() 75 | { 76 | time_t tt = time(NULL); 77 | tm *ptm = localtime(&tt); 78 | char buf[128] = {0}; 79 | sprintf(buf, "%d-%d-%d %d:%02d:%02d", 80 | (int)ptm->tm_year + 1900, 81 | (int)ptm->tm_mon + 1, 82 | (int)ptm->tm_mday, 83 | (int)ptm->tm_hour, 84 | (int)ptm->tm_min, 85 | (int)ptm->tm_sec); 86 | return std::string(buf); 87 | } 88 | 89 | COLORREF argb2abgr(const int &argb) 90 | { 91 | return (((argb & 0x000000ff) << 16) | (argb & 0x0000ff00) | ((argb & 0x00ff0000) >> 16)); 92 | } 93 | 94 | void CreateOffScreenSingleWatermark(HDC hdc, WaterMarkConfig &conf) 95 | { 96 | double alpha = (double)conf.angle / 180 * MATH_PI; 97 | int left = 0; 98 | int top = (int)(INIT_ONE_MARK_WIDTH * sin(alpha)); 99 | int xStep = (int)(conf.fontSize * sin(alpha)); 100 | int yStep = (int)(conf.fontSize * cos(alpha)) + 1; 101 | 102 | if (conf.showComputerName) 103 | { 104 | std::string computerName = GetComputerNameStr(); 105 | if (!TextOutA(hdc, left, top, computerName.c_str(), computerName.size())) 106 | { 107 | std::cout << "draw computer name failed,err:" << GetLastError() << std::endl; 108 | } 109 | left += xStep; 110 | top += yStep; 111 | } 112 | if (conf.showLocalTime) 113 | { 114 | std::string localTime = GetLocaltimeStr(); 115 | if (!TextOutA(hdc, left, top, localTime.c_str(), localTime.size())) 116 | { 117 | std::cout << "draw time failed,err:" << GetLastError() << std::endl; 118 | } 119 | left += xStep; 120 | top += yStep; 121 | } 122 | if (!conf.customText.empty()) 123 | { 124 | std::string &tmp = conf.customText; 125 | int lineChars = INIT_ONE_MARK_WIDTH / conf.fontSize; // coculate one line character number. 126 | 127 | for (int i = 0; i < conf.customText.size();) 128 | { 129 | std::string line; 130 | if ((i + lineChars) < conf.customText.size()) 131 | { 132 | line = std::string(tmp.begin() + i, tmp.begin() + i + lineChars); 133 | i += lineChars; 134 | } 135 | else 136 | { 137 | line = std::string(tmp.begin() + i, tmp.end()); 138 | i = conf.customText.size(); 139 | } 140 | if (!TextOutA(hdc, left, top, line.c_str(), line.size())) 141 | { 142 | std::cout << "draw user define string failed,err:" << GetLastError() << std::endl; 143 | } 144 | left += xStep; 145 | top += yStep; 146 | } 147 | } 148 | } 149 | 150 | void DrawWatermark(HDC hdc, WaterMarkConfig &conf) 151 | { 152 | if (conf.isInvisible) 153 | { 154 | conf.transparency = DEFAULT_TRANSPARENCY; 155 | conf.fontColor = DEFAULT_FONT_COLOR; 156 | } 157 | int textHeight = 0; 158 | if (conf.showComputerName) 159 | { 160 | textHeight += ONE_LINE_HEIGHT; 161 | } 162 | if (conf.showLocalTime) 163 | { 164 | textHeight += ONE_LINE_HEIGHT; 165 | } 166 | if (!conf.customText.empty()) 167 | { 168 | int lineChars = INIT_ONE_MARK_WIDTH / conf.fontSize; 169 | textHeight += ONE_LINE_HEIGHT * (conf.customText.size() / lineChars + 1); 170 | } 171 | 172 | // caculate real height of one mark. 173 | double alpha = (double)conf.angle / 180.0 * MATH_PI; 174 | int oneWidth = INIT_ONE_MARK_WIDTH * cos(alpha) + textHeight * sin(alpha) + conf.fontSize; // Add one more character width. 175 | int oneHeight = INIT_ONE_MARK_WIDTH * sin(alpha) + textHeight * cos(alpha) + conf.fontSize; 176 | 177 | static bool enablePrint = true; 178 | if (enablePrint) 179 | { 180 | std::cout << "Mark info:" 181 | << "\n isInvisible:" << conf.isInvisible 182 | << "\n window geometry:" << conf.wndGeometry.print() 183 | << "\n font color:" << std::hex << conf.fontColor << std::dec 184 | << "\n font size:" << conf.fontSize 185 | << "\n traqnsparency:" << conf.transparency 186 | << "\n angle:" << conf.angle 187 | << "\n show computer name:" << conf.showComputerName << (conf.showComputerName ? "(" + GetComputerNameStr() + ")" : "") 188 | << "\n show localtime:" << conf.showLocalTime << (conf.showLocalTime ? "(" + GetLocaltimeStr() + ")" : "") 189 | << "\n custom text:" << conf.customText 190 | << "\n off-screen single watermark:[" << oneWidth << "," << oneHeight << "]" 191 | << std::endl; 192 | enablePrint = false; 193 | } 194 | 195 | HFONT hfont = CreateFontA( 196 | conf.fontSize, 197 | conf.fontSize / 2, 198 | conf.angle * 10, 199 | 0, 200 | FW_HEAVY, 201 | 0, 202 | 0, 203 | 0, 204 | GB2312_CHARSET, 205 | OUT_DEFAULT_PRECIS, 206 | CLIP_DEFAULT_PRECIS, 207 | ANTIALIASED_QUALITY, 208 | DEFAULT_PITCH | FF_MODERN, 209 | "font"); 210 | 211 | HDC offscreenDc = CreateCompatibleDC(hdc); 212 | HBITMAP hBitmap = CreateCompatibleBitmap(hdc, oneWidth, oneHeight); 213 | 214 | SelectObject(offscreenDc, hBitmap); 215 | SelectObject(offscreenDc, hfont); 216 | SetTextColor(offscreenDc, argb2abgr(conf.fontColor)); 217 | SetBkColor(offscreenDc, BACKGROUND_COLOR); 218 | 219 | CreateOffScreenSingleWatermark(offscreenDc, conf); 220 | for (int i = conf.wndGeometry.y; i < conf.wndGeometry.h; i += oneHeight) 221 | { 222 | for (int j = conf.wndGeometry.x; j < conf.wndGeometry.w; j += oneWidth) 223 | { 224 | BitBlt(hdc, j, i, oneWidth, oneHeight, offscreenDc, 0, 0, SRCCOPY); 225 | } 226 | } 227 | 228 | DeleteObject(hfont); 229 | DeleteObject(hBitmap); 230 | DeleteObject(offscreenDc); 231 | offscreenDc = NULL; 232 | } 233 | 234 | LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) 235 | { 236 | switch (msg) 237 | { 238 | case WM_CREATE: 239 | { 240 | SetWindowLongW(hwnd, GWL_EXSTYLE, (GetWindowLongW(hwnd, GWL_EXSTYLE)) & ~WS_EX_APPWINDOW | WS_EX_TOOLWINDOW); 241 | SetTimer(hwnd, TIMER_1, AUTO_REFRESH_INTERVAL, NULL); 242 | SetTimer(hwnd, TIMER_2, SET_TOP_INTERVAL, NULL); 243 | std::cout << "create blind water mark window success." << std::endl; 244 | return 0; 245 | } 246 | case WM_TIMER: 247 | switch (wParam) 248 | { 249 | case TIMER_1: 250 | { 251 | int x = 0, y = 0, w = 0, h = 0; 252 | Geometry g = GetDesktopGeometry(); 253 | MoveWindow(hwnd, g.x, g.y, g.w, g.h, true); 254 | InvalidateRgn(hwnd, NULL, TRUE); 255 | UpdateWindow(hwnd); 256 | break; 257 | } 258 | case TIMER_2: 259 | SetWindowPos(hwnd, HWND_TOP, 0, 0, 0, 0, SWP_ASYNCWINDOWPOS | SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE); 260 | break; 261 | } 262 | return 0; 263 | case WM_PAINT: 264 | { 265 | DWORD time = GetTickCount(); 266 | PAINTSTRUCT ps; 267 | HDC hdc = BeginPaint(hwnd, &ps); 268 | DrawWatermark(hdc, g_waterMarkconf); 269 | // set window transparency 270 | // DWORD flag = g_waterMarkconf.isInvisible ? (LWA_ALPHA) : (LWA_COLORKEY | LWA_ALPHA); 271 | DWORD flag = LWA_ALPHA; 272 | SetLayeredWindowAttributes(hwnd, BACKGROUND_COLOR, g_waterMarkconf.transparency, flag); 273 | // Remove borders and title blocks 274 | SetWindowLongW(hwnd, GWL_STYLE, GetWindowLongW(hwnd, GWL_STYLE) & ~WS_CAPTION); 275 | // Set the key-rat event penetration 276 | SetWindowLongW(hwnd, GWL_EXSTYLE, (GetWindowLongW(hwnd, GWL_EXSTYLE)) | WS_EX_LAYERED | WS_EX_TRANSPARENT); 277 | // TOPMOST 278 | SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE); 279 | std::cout << "WM_PAINT use :" << GetTickCount() - time << " ms" << std::endl; 280 | EndPaint(hwnd, &ps); 281 | return 0; 282 | } 283 | case WM_DESTROY: 284 | PostQuitMessage(0); 285 | return 0; 286 | } 287 | 288 | return DefWindowProcW(hwnd, msg, wParam, lParam); 289 | } 290 | 291 | int WINAPI WinMain(HINSTANCE hThisInstance, HINSTANCE hPrevInstance, LPSTR lpszArgument, int iCmdShow) 292 | { 293 | // Ensure global uniqueness 294 | HANDLE g_hGlobalMutex = CreateMutexA(NULL, FALSE, GLOBAL_MUTEX_STRING); 295 | if (g_hGlobalMutex && GetLastError() == ERROR_ALREADY_EXISTS) 296 | { 297 | CloseHandle(g_hGlobalMutex); 298 | g_hGlobalMutex = NULL; 299 | return -1; 300 | } 301 | 302 | WNDCLASSA winClass = { 303 | CS_SAVEBITS, 304 | WndProc, 305 | 0, 306 | 0, 307 | hThisInstance, 308 | LoadIcon(NULL, IDI_APPLICATION), 309 | LoadCursor(NULL, IDC_ARROW), 310 | CreateSolidBrush(BACKGROUND_COLOR), 311 | NULL, 312 | "BlindMarkDilog"}; 313 | 314 | if (!RegisterClassA(&winClass)) 315 | { 316 | std::cout << "register window failed,BlindMarkDilog,err:" << GetLastError() << std::endl; 317 | return 0; 318 | } 319 | 320 | Geometry g = GetDesktopGeometry(); 321 | 322 | std::cout << "desktop grometry:" << g.print() << std::endl; 323 | HWND hwnd = CreateWindowW(L"BlindMarkDilog", L"BlindMarkWnd", WS_OVERLAPPED, 324 | g.x, g.y, g.w, g.h, 325 | NULL, NULL, hThisInstance, NULL); 326 | if (hwnd == NULL) 327 | { 328 | std::cout << "create window failed,err:" << GetLastError(); 329 | return 0; 330 | } 331 | 332 | ShowWindow(hwnd, iCmdShow); 333 | UpdateWindow(hwnd); 334 | 335 | MSG msg; 336 | while (GetMessageA(&msg, NULL, 0, 0)) 337 | { 338 | TranslateMessage(&msg); 339 | DispatchMessageA(&msg); 340 | } 341 | 342 | std::cout << "blind water mark quit" << std::endl; 343 | return msg.wParam; 344 | } --------------------------------------------------------------------------------