ASM_PNS_C/n8n.json
majkarol cd292e3198 Inicjalizacja repozytorium: koncepcja platformy AdReactions + specyfikacja AI
Zawartość:
- Koncepcja 0.1 oraz rozszerzona Koncepcja 0.2 (model domenowy, architektura,
  mapowanie cel→metryki→usługi AI, wymienialność modeli, model danych)
- Katalog 15 celów analiz kognitywnych
- Specyfikacja procesu AI + opis procesów źródłowych
- Pytania do koncepcji
- Materiały źródłowe: info.md, n8n.json

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 22:03:32 +02:00

823 lines
No EOL
38 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
"name": "v1.6 Test",
"nodes": [
{
"parameters": {
"formTitle": "Form input data",
"formFields": {
"values": [
{
"fieldLabel": "image",
"fieldType": "file"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.formTrigger",
"typeVersion": 2.5,
"position": [
-1200,
160
],
"id": "bc08484e-ccbe-4ccf-b6dc-9f8bd59d1197",
"name": "Form input data",
"webhookId": "afa8943e-cab2-4554-b64d-2c888ecfd9c3"
},
{
"parameters": {
"method": "POST",
"url": "=http://{{ $json.LLaVA }}/llava-api/v1/get-object-types",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "accept",
"value": "application/json"
},
{
"name": "Authorization",
"value": "=Bearer {{ $json.Token }}"
}
]
},
"sendBody": true,
"contentType": "multipart-form-data",
"bodyParameters": {
"parameters": [
{
"name": "temperature",
"value": "0.2"
},
{
"name": "top_p",
"value": "0.9"
},
{
"parameterType": "formBinaryData",
"name": "image",
"inputDataFieldName": "=image"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
64,
-240
],
"id": "fa89e8ca-2669-48f2-bdcf-e4d2aeee1311",
"name": "LLaVA-based object types extractor",
"onError": "continueRegularOutput"
},
{
"parameters": {
"method": "POST",
"url": "=http://{{ $json.Grounding_DINO }}/grounding-dino-api/v1/get-objects-bboxes ",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "accept",
"value": "application/json"
},
{
"name": "Authorization",
"value": "=Bearer {{ $json.Token }}"
}
]
},
"sendBody": true,
"contentType": "multipart-form-data",
"bodyParameters": {
"parameters": [
{
"name": "object_types",
"value": "={{ $json.labels_string }}"
},
{
"name": "max_objects",
"value": "50"
},
{
"name": "base64",
"value": "false"
},
{
"parameterType": "formBinaryData",
"name": "image",
"inputDataFieldName": "image"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
800,
-16
],
"id": "6cfd2993-30d9-4905-a643-48a73273c60c",
"name": "Grounding DINO"
},
{
"parameters": {
"operation": "binaryToPropery",
"binaryPropertyName": "image",
"destinationKey": "image_input",
"options": {}
},
"type": "n8n-nodes-base.extractFromFile",
"typeVersion": 1.1,
"position": [
-384,
560
],
"id": "eb0dc17a-cedb-4ea2-a28e-c9aac74f188e",
"name": "base64"
},
{
"parameters": {
"method": "POST",
"url": "=http://{{ $json.DeepGaze }}/v1/predict-heatmap ",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "accept",
"value": "application/json"
},
{
"name": "Authorization",
"value": "=Bearer {{ $json.Token }}"
}
]
},
"sendBody": true,
"contentType": "multipart-form-data",
"bodyParameters": {
"parameters": [
{
"parameterType": "formBinaryData",
"name": "image",
"inputDataFieldName": "image"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
96,
352
],
"id": "4e3419b0-dae9-4bba-8729-8798d4d5f289",
"name": "DeepGaze",
"onError": "continueRegularOutput"
},
{
"parameters": {
"jsCode": "const item = $input.first().json;\n\nconst binaryData = Buffer.from(item.heatmap_b64, 'base64');\n\nreturn [{\n json: item,\n binary: {\n image: await this.helpers.prepareBinaryData(\n binaryData,\n 'heatmap.png',\n 'image/png'\n )\n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1120,
352
],
"id": "456beac3-734b-4cc2-9006-3b671462b9ed",
"name": "Image"
},
{
"parameters": {},
"type": "n8n-nodes-base.merge",
"typeVersion": 3.2,
"position": [
1120,
576
],
"id": "e3ea6d0e-cc9e-491b-9a52-e83f733d59ed",
"name": "Merge"
},
{
"parameters": {
"content": "## Inverted heatmap - DeepGaze\nPrezentacja heatmap nałożonej na grafikę źródłową",
"height": 112,
"width": 464,
"color": 4
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
1424,
720
],
"id": "d4207e63-5241-4b70-a01a-bb4d52e1641e",
"name": "Sticky Note"
},
{
"parameters": {},
"type": "n8n-nodes-base.merge",
"typeVersion": 3.2,
"position": [
1120,
144
],
"id": "b8dec319-2fa6-4655-afbb-87ab03a7d167",
"name": "Merge1"
},
{
"parameters": {
"content": "## Object Detection\nObiekty nałożone na grafikę źródłową",
"height": 112,
"width": 464,
"color": 4
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
1424,
288
],
"id": "02ee310f-191b-4e74-b562-1ee1b030560e",
"name": "Sticky Note1"
},
{
"parameters": {
"content": "## Object Listing\nLista obiektów wraz z opisem grafiki",
"height": 112,
"width": 464,
"color": 4
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-96,
-384
],
"id": "426c4aca-e358-4aeb-8de1-182eaff75a44",
"name": "Sticky Note2"
},
{
"parameters": {
"mode": "combine",
"combineBy": "combineAll",
"options": {}
},
"type": "n8n-nodes-base.merge",
"typeVersion": 3.2,
"position": [
-384,
-16
],
"id": "7cd356f2-501a-418a-b809-67f520433273",
"name": "Merge2"
},
{
"parameters": {
"operation": "get",
"dataTableId": {
"__rl": true,
"value": "vNV6H2oqepIaXaDj",
"mode": "list",
"cachedResultName": "Credentials",
"cachedResultUrl": "/projects/OZaIb3cfHKXhndDW/datatables/vNV6H2oqepIaXaDj"
},
"limit": 1
},
"type": "n8n-nodes-base.dataTable",
"typeVersion": 1.1,
"position": [
-848,
-32
],
"id": "7c2ee8f0-8eab-4c80-bca8-818e5335feff",
"name": "Credentials"
},
{
"parameters": {
"mode": "combine",
"combineBy": "combineAll",
"options": {}
},
"type": "n8n-nodes-base.merge",
"typeVersion": 3.2,
"position": [
560,
-16
],
"id": "9298bcb8-d2f6-4d42-8e03-13b556495cab",
"name": "Merge5"
},
{
"parameters": {
"content": "## Heatmap\n",
"height": 112,
"width": 464
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
1424,
1632
],
"id": "786650ff-5fd8-4a83-9ad1-9fb22041789c",
"name": "Sticky Note3"
},
{
"parameters": {
"content": "## Interwały\n30 sek, 6 obrazów\n",
"height": 112,
"width": 464
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
1904,
720
],
"id": "f5e0761f-b4df-4b27-910d-d7cecf3110c2",
"name": "Sticky Note4"
},
{
"parameters": {
"content": "## Interwały\n30 sek, 6 obrazów\n20 sek. do 0,5 sekundy",
"height": 112,
"width": 464
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
1904,
1632
],
"id": "05d80ebd-4a8d-435f-b125-96d61162b1e1",
"name": "Sticky Note5"
},
{
"parameters": {
"content": "## Ścieżki fiksacji\n",
"height": 112,
"width": 464
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
1424,
1920
],
"id": "8671e093-3278-4d7d-b028-7066a634aa10",
"name": "Sticky Note6"
},
{
"parameters": {
"content": "## Interwały\n???\n",
"height": 112,
"width": 464
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
1904,
1920
],
"id": "3d9a385e-377a-494d-be51-57c02eb6bbc6",
"name": "Sticky Note7"
},
{
"parameters": {
"content": "## Mapa emocji\n",
"height": 112,
"width": 464
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
1424,
2176
],
"id": "be6a9c40-a7d9-489a-8452-b1dd79efaf01",
"name": "Sticky Note8"
},
{
"parameters": {
"jsCode": "const labels = $input.first().json.labels;\nreturn [{ json: { labels_string: labels.join(\", \") } }];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
256,
-240
],
"id": "040cb049-68ca-46ca-b46f-06ecce243387",
"name": "Labels string"
},
{
"parameters": {
"jsCode": "const items = $input.all();\n\nconst originalB64 = items[0].json.image_input;\nconst heatmapB64 = items[1].json.heatmap_b64;\nconst heatmapMime = items[1].json.heatmap_mime;\nconst width = items[1].json.width;\nconst height = items[1].json.height;\n\nconst html = `<!DOCTYPE html>\n<html lang=\"pl\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>Heatmap Overlay Viewer</title>\n<style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body {\n background: #1a1a2e;\n color: #e0e0e0;\n font-family: 'Segoe UI', system-ui, sans-serif;\n display: flex;\n flex-direction: column;\n align-items: center;\n min-height: 100vh;\n padding: 24px;\n }\n h1 {\n font-size: 1.4rem;\n font-weight: 600;\n margin-bottom: 20px;\n color: #fff;\n letter-spacing: 0.5px;\n }\n .viewer {\n position: relative;\n width: ${width}px;\n max-width: 95vw;\n aspect-ratio: ${width} / ${height};\n border-radius: 12px;\n overflow: hidden;\n box-shadow: 0 8px 32px rgba(0,0,0,0.5);\n }\n .viewer img {\n position: absolute;\n top: 0; left: 0;\n width: 100%;\n height: 100%;\n object-fit: contain;\n }\n #heatmap {\n opacity: 0.5;\n transition: opacity 0.05s ease;\n }\n .controls {\n margin-top: 24px;\n background: #16213e;\n padding: 20px 32px;\n border-radius: 12px;\n display: flex;\n align-items: center;\n gap: 16px;\n box-shadow: 0 4px 16px rgba(0,0,0,0.3);\n max-width: 95vw;\n }\n .controls label {\n font-size: 0.95rem;\n white-space: nowrap;\n }\n input[type=\"range\"] {\n -webkit-appearance: none;\n width: 300px;\n height: 6px;\n border-radius: 3px;\n background: linear-gradient(90deg, #0f3460, #e94560);\n outline: none;\n cursor: pointer;\n }\n input[type=\"range\"]::-webkit-slider-thumb {\n -webkit-appearance: none;\n width: 20px; height: 20px;\n border-radius: 50%;\n background: #fff;\n box-shadow: 0 2px 6px rgba(0,0,0,0.4);\n cursor: grab;\n }\n input[type=\"range\"]::-moz-range-thumb {\n width: 20px; height: 20px;\n border-radius: 50%;\n background: #fff;\n border: none;\n box-shadow: 0 2px 6px rgba(0,0,0,0.4);\n cursor: grab;\n }\n #opacityValue {\n font-weight: 700;\n font-size: 1.1rem;\n min-width: 48px;\n text-align: center;\n color: #e94560;\n }\n .hint {\n margin-top: 12px;\n font-size: 0.8rem;\n color: #666;\n }\n</style>\n</head>\n<body>\n\n<h1>Heatmap overlay viewer</h1>\n\n<div class=\"viewer\">\n <img id=\"original\" src=\"data:image/png;base64,${originalB64}\" alt=\"Original\" />\n <img id=\"heatmap\" src=\"data:${heatmapMime};base64,${heatmapB64}\" alt=\"Heatmap\" />\n</div>\n\n<div class=\"controls\">\n <label for=\"slider\">Przezroczystość heatmapy:</label>\n <input type=\"range\" id=\"slider\" min=\"0\" max=\"100\" value=\"50\" />\n <span id=\"opacityValue\">50%</span>\n</div>\n\n<p class=\"hint\">Przesuń suwak w lewo, aby zobaczyć oryginał · w prawo, aby zobaczyć heatmapę</p>\n\n<script>\n const slider = document.getElementById('slider');\n const heatmap = document.getElementById('heatmap');\n const display = document.getElementById('opacityValue');\n\n slider.addEventListener('input', () => {\n const val = slider.value;\n heatmap.style.opacity = val / 100;\n display.textContent = val + '%';\n });\n</script>\n</body>\n</html>`;\n\nconst binaryData = await this.helpers.prepareBinaryData(\n Buffer.from(html, 'utf-8'),\n 'heatmap_viewer.html',\n 'text/html'\n);\n\nreturn [{\n json: {\n filename: 'heatmap_viewer.html',\n width,\n height,\n model: items[1].json.model\n },\n binary: { file: binaryData }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1424,
576
],
"id": "fbc15b23-9640-48da-b79f-33328a1c92dd",
"name": "Inverted heatmap"
},
{
"parameters": {
"method": "POST",
"url": "http://1.208.108.242:58951/unisal/process-image",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "as_base64",
"value": "true"
}
]
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "accept",
"value": "application/json"
},
{
"name": "X-API-Key",
"value": "={{ $json.Token }}"
}
]
},
"sendBody": true,
"contentType": "multipart-form-data",
"bodyParameters": {
"parameters": [
{
"parameterType": "formBinaryData",
"name": "file",
"inputDataFieldName": "image"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
96,
832
],
"id": "edd412d8-cbc2-48ab-8df7-2ef047ee9f5e",
"name": "unisal - inverted heatmap",
"onError": "continueRegularOutput"
},
{
"parameters": {
"method": "POST",
"url": "http://1.208.108.242:58951/asmModel/process-image",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "prompt",
"value": "Describe the image in the style of a polished marketing ad."
},
{
"name": "as_raw_output",
"value": "false"
},
{
"name": "max_new_tokens",
"value": "100"
}
]
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "accept",
"value": "application/json"
},
{
"name": "X-API-Key",
"value": "={{ $json.Token }}"
}
]
},
"sendBody": true,
"contentType": "multipart-form-data",
"bodyParameters": {
"parameters": [
{
"parameterType": "formBinaryData",
"name": "file",
"inputDataFieldName": "image"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
96,
1040
],
"id": "91aabc80-b814-4c31-9796-e9acdad65192",
"name": "asmModel - inverted heatmap",
"onError": "continueRegularOutput"
},
{
"parameters": {
"numberInputs": 4
},
"type": "n8n-nodes-base.merge",
"typeVersion": 3.2,
"position": [
1120,
944
],
"id": "2abef155-3c6f-4c64-9aaa-a5bc12ef2c43",
"name": "Merge6"
},
{
"parameters": {
"content": "## Inverted heatmap - Compare\nPrezentacja heatmap nałożonej na grafikę źródłową",
"height": 112,
"width": 464,
"color": 4
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
1424,
1120
],
"id": "b3a754d2-c3ff-4dfd-a8a0-71c8a37d3bb5",
"name": "Sticky Note9"
},
{
"parameters": {
"jsCode": "// n8n Code Node Generate Inverted Heatmap HTML (Canvas pixel-blend)\n// Input items (4):\n// [0] image_input original image (base64 PNG)\n// [1] heatmap_b64 DeepGaze heatmap (base64 PNG), heatmap_mime\n// [2] data unisal heatmap (base64 JPEG), mimetype\n// [3] image_detailed asmModel detailed (base64), image_general asmModel general, mimetype\n\nconst imgInput = items[0].json.image_input;\nconst deepGazeB64 = items[1].json.heatmap_b64;\nconst deepGazeMime = items[1].json.heatmap_mime || 'image/png';\nconst unisalB64 = items[2].json.data;\nconst unisalMime = items[2].json.mimetype || 'image/jpeg';\nconst detailedB64 = items[3].json.image_detailed;\nconst generalB64 = items[3].json.image_general;\nconst asmMime = items[3].json.mimetype || 'image/jpeg';\n\nconst origMime = imgInput.startsWith('/9j/') ? 'image/jpeg' : 'image/png';\n\nconst html = `<!DOCTYPE html>\n<html lang=\"pl\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>Inverted Heatmap</title>\n<style>\n * { box-sizing: border-box; margin: 0; padding: 0; }\n body {\n font-family: 'Segoe UI', Arial, sans-serif;\n background: #1a1a2e;\n color: #e0e0e0;\n min-height: 100vh;\n padding: 24px;\n }\n h1 {\n text-align: center;\n font-size: 2rem;\n letter-spacing: 2px;\n margin-bottom: 24px;\n color: #a0c4ff;\n text-shadow: 0 0 12px #3a7bd560;\n }\n .slider-container {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 16px;\n margin-bottom: 32px;\n }\n .slider-container label {\n font-size: 1rem;\n color: #a0c4ff;\n white-space: nowrap;\n }\n #opacitySlider {\n -webkit-appearance: none;\n appearance: none;\n width: 320px;\n height: 6px;\n border-radius: 3px;\n background: linear-gradient(to right, #3a7bd5, #a0c4ff);\n outline: none;\n cursor: pointer;\n }\n #opacitySlider::-webkit-slider-thumb {\n -webkit-appearance: none;\n appearance: none;\n width: 20px;\n height: 20px;\n border-radius: 50%;\n background: #a0c4ff;\n box-shadow: 0 0 6px #3a7bd5;\n cursor: pointer;\n }\n #opacityValue {\n font-size: 1rem;\n color: #fff;\n min-width: 40px;\n text-align: left;\n }\n #status {\n text-align: center;\n color: #a0c4ff88;\n font-size: 0.9rem;\n margin-bottom: 16px;\n min-height: 1.2em;\n }\n .grid {\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n gap: 20px;\n max-width: 1400px;\n margin: 0 auto;\n }\n .card {\n background: #16213e;\n border-radius: 12px;\n overflow: hidden;\n box-shadow: 0 4px 24px #0008;\n display: flex;\n flex-direction: column;\n }\n .card-title {\n text-align: center;\n padding: 10px 0 8px;\n font-size: 1.05rem;\n font-weight: 600;\n letter-spacing: 1px;\n color: #a0c4ff;\n background: #0f3460;\n }\n canvas {\n display: block;\n width: 100%;\n height: auto;\n }\n @media (max-width: 700px) {\n .grid { grid-template-columns: 1fr; }\n }\n</style>\n</head>\n<body>\n<h1>Inverted Heatmap</h1>\n\n<div class=\"slider-container\">\n <label for=\"opacitySlider\">Przezroczystość heatmapy:</label>\n <input type=\"range\" id=\"opacitySlider\" min=\"0\" max=\"100\" value=\"70\">\n <span id=\"opacityValue\">70%</span>\n</div>\n<div id=\"status\">Ładowanie obrazów…</div>\n\n<div class=\"grid\">\n <div class=\"card\">\n <div class=\"card-title\">DeepGaze</div>\n <canvas id=\"c0\"></canvas>\n </div>\n <div class=\"card\">\n <div class=\"card-title\">unisal</div>\n <canvas id=\"c1\"></canvas>\n </div>\n <div class=\"card\">\n <div class=\"card-title\">asmModel detailed</div>\n <canvas id=\"c2\"></canvas>\n </div>\n <div class=\"card\">\n <div class=\"card-title\">asmModel general</div>\n <canvas id=\"c3\"></canvas>\n </div>\n</div>\n\n<script>\n // ── Embedded image data ──────────────────────────────────────────────────\n const SRC_ORIG = 'data:${origMime};base64,${imgInput}';\n const SRC_DEEPGAZE = 'data:${deepGazeMime};base64,${deepGazeB64}';\n const SRC_UNISAL = 'data:${unisalMime};base64,${unisalB64}';\n const SRC_DETAILED = 'data:${asmMime};base64,${detailedB64}';\n const SRC_GENERAL = 'data:${asmMime};base64,${generalB64}';\n\n const CARD_DEFS = [\n { canvasId: 'c0', heatSrc: SRC_DEEPGAZE },\n { canvasId: 'c1', heatSrc: SRC_UNISAL },\n { canvasId: 'c2', heatSrc: SRC_DETAILED },\n { canvasId: 'c3', heatSrc: SRC_GENERAL },\n ];\n\n // ── Helpers ──────────────────────────────────────────────────────────────\n function loadImg(src) {\n return new Promise((resolve, reject) => {\n const img = new Image();\n img.onload = () => resolve(img);\n img.onerror = reject;\n img.src = src;\n });\n }\n\n function imgToPixels(img, w, h) {\n const c = document.createElement('canvas');\n c.width = w; c.height = h;\n const ctx = c.getContext('2d');\n ctx.drawImage(img, 0, 0, w, h);\n return ctx.getImageData(0, 0, w, h).data; // Uint8ClampedArray\n }\n\n // ── Cache (populated once) ───────────────────────────────────────────────\n // Each entry: { origData: Uint8ClampedArray, heatGray: Float32Array, w, h, canvas }\n const cache = [];\n\n async function init() {\n const origImg = await loadImg(SRC_ORIG);\n const heatImgs = await Promise.all(CARD_DEFS.map(d => loadImg(d.heatSrc)));\n\n const W = origImg.naturalWidth;\n const H = origImg.naturalHeight;\n\n // Original pixels (shared by all cards)\n const origData = imgToPixels(origImg, W, H);\n\n for (let i = 0; i < CARD_DEFS.length; i++) {\n const canvas = document.getElementById(CARD_DEFS[i].canvasId);\n canvas.width = W;\n canvas.height = H;\n\n // Heatmap → grayscale luminance array (0..1)\n const heatRaw = imgToPixels(heatImgs[i], W, H);\n const heatGray = new Float32Array(W * H);\n for (let p = 0; p < W * H; p++) {\n const o = p * 4;\n // Luma (perceptual weights)\n heatGray[p] = (heatRaw[o] * 0.299 + heatRaw[o+1] * 0.587 + heatRaw[o+2] * 0.114) / 255;\n }\n\n cache.push({ origData, heatGray, W, H, canvas });\n }\n\n document.getElementById('status').textContent = '';\n drawAll(0.70);\n }\n\n // ── Pixel blend ──────────────────────────────────────────────────────────\n // t=0 → original image\n // t=1 → white where attention (heatGray≈1), black where not (heatGray≈0)\n // Formula: result = orig*(1-t) + heatGray*255*t\n function drawAll(t) {\n const t1 = 1 - t;\n for (const { origData, heatGray, W, H, canvas } of cache) {\n const ctx = canvas.getContext('2d');\n const result = ctx.createImageData(W, H);\n const d = result.data;\n for (let p = 0; p < W * H; p++) {\n const o = p * 4;\n const wb = heatGray[p] * 255 * t;\n d[o] = origData[o] * t1 + wb;\n d[o+1] = origData[o+1] * t1 + wb;\n d[o+2] = origData[o+2] * t1 + wb;\n d[o+3] = 255;\n }\n ctx.putImageData(result, 0, 0);\n }\n }\n\n // ── Slider ───────────────────────────────────────────────────────────────\n const slider = document.getElementById('opacitySlider');\n const valueLabel = document.getElementById('opacityValue');\n\n slider.addEventListener('input', () => {\n valueLabel.textContent = slider.value + '%';\n drawAll(slider.value / 100);\n });\n\n // ── Boot ─────────────────────────────────────────────────────────────────\n init().catch(err => {\n document.getElementById('status').textContent = 'Błąd ładowania obrazów: ' + err;\n });\n</script>\n</body>\n</html>`;\n\n// Zwróć jako plik binarny do pobrania z n8n\nconst binaryData = await this.helpers.prepareBinaryData(\n Buffer.from(html, 'utf8'),\n 'inverted_heatmap.html',\n 'text/html'\n);\n\nreturn [{ json: {}, binary: { data: binaryData } }];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1424,
976
],
"id": "fe201da2-822e-4433-973d-d14d3e72421e",
"name": "Inverted heatmap - Compare"
},
{
"parameters": {
"jsCode": "const items = $input.all();\n\nconst imageB64 = items[0].json.image_input;\nconst detections = items[1].json.objects_bboxes || [];\n\n// --- Odczyt rzeczywistych wymiarów obrazu z nagłówka binarnego (base64) ---\n// Eliminuje zależność od pól width/height, których może nie być w danych\nconst imgBuffer = Buffer.from(imageB64, 'base64');\n\nlet imgWidth, imgHeight, mimeType;\n\nif (\n imgBuffer[0] === 0x89 && imgBuffer[1] === 0x50 &&\n imgBuffer[2] === 0x4E && imgBuffer[3] === 0x47\n) {\n // PNG — wymiary w bajtach 16-23 (big-endian uint32)\n mimeType = 'image/png';\n imgWidth = imgBuffer.readUInt32BE(16);\n imgHeight = imgBuffer.readUInt32BE(20);\n} else if (imgBuffer[0] === 0xFF && imgBuffer[1] === 0xD8) {\n // JPEG — szukamy markera SOF0 (0xC0) lub SOF2 (0xC2)\n mimeType = 'image/jpeg';\n let offset = 2;\n let found = false;\n while (offset + 3 < imgBuffer.length) {\n if (imgBuffer[offset] !== 0xFF) break;\n const marker = imgBuffer[offset + 1];\n if (marker === 0xC0 || marker === 0xC2) {\n imgHeight = imgBuffer.readUInt16BE(offset + 5);\n imgWidth = imgBuffer.readUInt16BE(offset + 7);\n found = true;\n break;\n }\n // Przejdź do następnego markera\n const segLen = imgBuffer.readUInt16BE(offset + 2);\n offset += 2 + segLen;\n }\n if (!found) {\n imgWidth = items[1].json.width || 839;\n imgHeight = items[1].json.height || 1190;\n }\n} else {\n // Nieznany format — fallback na dane z JSON lub wartości domyślne\n mimeType = 'image/png';\n imgWidth = items[1].json.width || 839;\n imgHeight = items[1].json.height || 1190;\n}\n\n// --- Dynamiczna paleta kolorów ---\nconst palette = [\n '#00e5ff', '#ff4081', '#ffea00', '#76ff03', '#d500f9',\n '#ff6d00', '#00bfa5', '#304ffe', '#ff1744', '#64ffda',\n '#f50057', '#00b0ff', '#ffc400', '#69f0ae', '#e040fb',\n '#ff9100', '#18ffff', '#7c4dff', '#ff3d00', '#b2ff59',\n];\n\nconst uniqueTypes = [...new Set(detections.map(d => d.object_type))];\nconst colors = {};\nuniqueTypes.forEach((type, i) => {\n colors[type] = palette[i % palette.length];\n});\n\n// --- Generowanie boksów z pozycjonowaniem procentowym ---\n// Kluczowa poprawka: używamy % zamiast px, dzięki czemu boxy\n// skalują się razem z obrazem przy każdej rozdzielczości ekranu.\nconst boxesHtml = detections.map(det => {\n const [x1, y1, x2, y2] = det.bbox_coords_xyxy;\n const color = colors[det.object_type];\n const score = (det.score * 100).toFixed(1);\n\n const left = ((x1 / imgWidth) * 100).toFixed(4);\n const top = ((y1 / imgHeight) * 100).toFixed(4);\n const width = (((x2 - x1) / imgWidth) * 100).toFixed(4);\n const height = (((y2 - y1) / imgHeight)* 100).toFixed(4);\n\n return `\n <div class=\"bbox\" data-type=\"${det.object_type}\" style=\"\n left:${left}%; top:${top}%;\n width:${width}%; height:${height}%;\n border-color:${color};\n \">\n <span class=\"label\" style=\"background:${color};\">${det.object_type} ${score}%</span>\n </div>`;\n}).join('\\n');\n\n// --- Legenda i filtry ---\nconst legend = uniqueTypes.map(type =>\n `<span class=\"legend-item\"><span class=\"legend-dot\" style=\"background:${colors[type]};\"></span>${type}</span>`\n).join('');\n\nconst filters = uniqueTypes.map(type => {\n const count = detections.filter(d => d.object_type === type).length;\n return `<button class=\"filter-btn active\" data-filter=\"${type}\" style=\"--c:${colors[type]};\">${type} <small>(${count})</small></button>`;\n}).join('\\n');\n\nconst html = `<!DOCTYPE html>\n<html lang=\"pl\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>Object Detection Viewer</title>\n<style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body {\n background: #0d1117;\n color: #e6edf3;\n font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;\n display: flex;\n flex-direction: column;\n align-items: center;\n padding: 24px;\n min-height: 100vh;\n }\n h1 { font-size: 1.3rem; font-weight: 600; margin-bottom: 16px; color: #fff; }\n\n .toolbar {\n display: flex; flex-wrap: wrap; align-items: center; gap: 12px;\n background: #161b22; padding: 12px 20px; border-radius: 10px;\n margin-bottom: 20px; box-shadow: 0 2px 12px rgba(0,0,0,.4);\n width: 100%; max-width: min(${imgWidth}px, 95vw);\n }\n .legend-item {\n display: inline-flex; align-items: center; gap: 6px;\n font-size: .85rem; margin-right: 8px;\n }\n .legend-dot { width: 12px; height: 12px; border-radius: 3px; display: inline-block; }\n\n .filter-btn {\n padding: 4px 12px; border-radius: 6px;\n border: 2px solid var(--c, #30363d);\n background: transparent; color: #e6edf3; font-size: .82rem;\n cursor: pointer; transition: background .15s;\n }\n .filter-btn.active { background: var(--c, #238636); color: #000; }\n .filter-btn:hover { opacity: .85; }\n\n /* Kontener obrazu — szerokość ograniczona do 95vw lub rzeczywistej szerokości obrazu */\n .canvas-wrap {\n position: relative;\n width: min(${imgWidth}px, 95vw);\n /* aspect-ratio zapewnia proporcjonalne skalowanie w pionie */\n aspect-ratio: ${imgWidth} / ${imgHeight};\n border-radius: 10px;\n overflow: hidden;\n box-shadow: 0 8px 30px rgba(0,0,0,.6);\n background: #000;\n }\n .canvas-wrap img {\n /* Obraz wypełnia cały kontener; object-fit:fill bo aspect-ratio już gwarantuje proporcje */\n position: absolute;\n inset: 0;\n width: 100%; height: 100%;\n display: block;\n }\n\n /* Boxy — pozycja i rozmiar jako % kontenera, skalują się razem z obrazem */\n .bbox {\n position: absolute;\n border: 2px solid;\n border-radius: 3px;\n pointer-events: none;\n transition: opacity .2s;\n }\n .bbox.hidden { opacity: 0; pointer-events: none; }\n\n .label {\n position: absolute;\n top: -22px; left: -2px;\n padding: 2px 7px;\n font-size: .7rem;\n font-weight: 700;\n color: #000;\n border-radius: 3px 3px 3px 0;\n white-space: nowrap;\n letter-spacing: .3px;\n line-height: 1.4;\n }\n\n .stats {\n margin-top: 16px; font-size: .8rem; color: #8b949e;\n width: 100%; max-width: min(${imgWidth}px, 95vw);\n }\n</style>\n</head>\n<body>\n\n<h1>🎯 Object Detection — ${detections.length} obiektów</h1>\n\n<div class=\"toolbar\">\n <span style=\"font-size:.85rem;color:#8b949e;\">Filtruj:</span>\n ${filters}\n <span style=\"flex:1;\"></span>\n <span style=\"font-size:.8rem;color:#8b949e;\">Legenda:</span>\n ${legend}\n</div>\n\n<div class=\"canvas-wrap\" id=\"canvas\">\n <img src=\"data:${mimeType};base64,${imageB64}\" alt=\"Source\" />\n ${boxesHtml}\n</div>\n\n<div class=\"stats\">\n Model: <strong>${items[1].json.model_id || '—'}</strong> ·\n Threshold: ${items[1].json.threshold ?? '—'} ·\n Rozdzielczość: ${imgWidth}×${imgHeight} ·\n Typy: ${uniqueTypes.join(', ')}\n</div>\n\n<script>\ndocument.querySelectorAll('.filter-btn').forEach(btn => {\n btn.addEventListener('click', () => {\n btn.classList.toggle('active');\n const type = btn.dataset.filter;\n const show = btn.classList.contains('active');\n document.querySelectorAll('.bbox[data-type=\"' + type + '\"]').forEach(b => {\n b.classList.toggle('hidden', !show);\n });\n });\n});\n</script>\n</body>\n</html>`;\n\nconst binaryData = await this.helpers.prepareBinaryData(\n Buffer.from(html, 'utf-8'),\n 'detection_viewer.html',\n 'text/html'\n);\n\nreturn [{\n json: {\n filename: 'detection_viewer.html',\n total_objects: detections.length,\n types: uniqueTypes,\n model: items[1].json.model_id,\n image_width: imgWidth,\n image_height: imgHeight,\n mime_type: mimeType,\n },\n binary: { file: binaryData }\n}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1424,
144
],
"id": "fa626aa0-64a4-4818-83b3-ac1f88228694",
"name": "Object Detection"
}
],
"pinData": {},
"connections": {
"Form input data": {
"main": [
[
{
"node": "base64",
"type": "main",
"index": 0
},
{
"node": "Merge2",
"type": "main",
"index": 1
},
{
"node": "Credentials",
"type": "main",
"index": 0
}
]
]
},
"DeepGaze": {
"main": [
[
{
"node": "Image",
"type": "main",
"index": 0
},
{
"node": "Merge",
"type": "main",
"index": 1
},
{
"node": "Merge6",
"type": "main",
"index": 1
}
]
]
},
"Image": {
"main": [
[]
]
},
"Merge": {
"main": [
[
{
"node": "Inverted heatmap",
"type": "main",
"index": 0
}
]
]
},
"base64": {
"main": [
[
{
"node": "Merge",
"type": "main",
"index": 0
},
{
"node": "Merge1",
"type": "main",
"index": 0
},
{
"node": "Merge6",
"type": "main",
"index": 0
}
]
]
},
"Grounding DINO": {
"main": [
[
{
"node": "Merge1",
"type": "main",
"index": 1
}
]
]
},
"Merge1": {
"main": [
[
{
"node": "Object Detection",
"type": "main",
"index": 0
}
]
]
},
"Merge2": {
"main": [
[
{
"node": "LLaVA-based object types extractor",
"type": "main",
"index": 0
},
{
"node": "unisal - inverted heatmap",
"type": "main",
"index": 0
},
{
"node": "Merge5",
"type": "main",
"index": 1
},
{
"node": "DeepGaze",
"type": "main",
"index": 0
},
{
"node": "asmModel - inverted heatmap",
"type": "main",
"index": 0
}
]
]
},
"Credentials": {
"main": [
[
{
"node": "Merge2",
"type": "main",
"index": 0
}
]
]
},
"LLaVA-based object types extractor": {
"main": [
[
{
"node": "Labels string",
"type": "main",
"index": 0
}
]
]
},
"Merge5": {
"main": [
[
{
"node": "Grounding DINO",
"type": "main",
"index": 0
}
]
]
},
"Labels string": {
"main": [
[
{
"node": "Merge5",
"type": "main",
"index": 0
}
]
]
},
"Inverted heatmap": {
"main": [
[]
]
},
"unisal - inverted heatmap": {
"main": [
[
{
"node": "Merge6",
"type": "main",
"index": 2
}
]
]
},
"asmModel - inverted heatmap": {
"main": [
[
{
"node": "Merge6",
"type": "main",
"index": 3
}
]
]
},
"Merge6": {
"main": [
[
{
"node": "Inverted heatmap - Compare",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1",
"binaryMode": "separate",
"availableInMCP": false
},
"versionId": "5e97360b-e09b-43a9-962f-91090d292f8c",
"meta": {
"instanceId": "1e4ac0fc2b33692d40da602263ad5f150100624f047385c3b326b30a4678b299"
},
"id": "ByacxQXqRD7pGkWW",
"tags": []
}