Thread Art is a web application for visualizing images as paths constructed from straight lines between points on the image boundaries.
- Path Generation: Create a sequence of points connected by straight lines
- Result Visualization: Render the processed image with color vertex markers
- Data Export: Save the path in text format for later use
- Asynchronous Processing: Process large images without blocking the main thread
- Priority System: Task Queue with the ability to process multiple files in parallel
| Original Image | Path Result |
|---|---|
![]() |
![]() |
Points per Boundary: 300
Algorithm Steps: 4000
Padding: 10px
Contrast: Automatically Calculated
- .NET 10.0 SDK
- Docker and Docker Compose
- PostgreSQL Database (enabled via docker-compose)
- Cloning the Repository:
git clone https://github.com/filippov112/thread-art.git
cd thread-art- Setting Environment Variables:
cp .env.example .env- Running via Docker Compose:
docker-compose up- Initial Database Setup:
# Migrations will be applied automatically on first run- Application Access:
- HTTP:
http://localhost:8080 - HTTPS:
https://localhost:7005 - Swagger UI API:
https://localhost:7005/swagger - Scalar API:
https://localhost:7005/scalar
dotnet restore
dotnet ef database update
dotnet run --project src/WebEdit the file src/Web/appsettings.json:
{
"Storage": {
"FolderPath": "storage",
"StaticFiles": "wwwroot",
"CleanupIntervalHours": 0.1,
"FileAgeHours": 1.1
},
"Processing": {
"MaxConcurrency": 0
}
}| Category | Technologies | Version |
|---|---|---|
| Language | C# | 12+ |
| Framework | ASP.NET Core | 10.0 |
| Database | PostgreSQL | 16 |
| ORM | Entity Framework Core | 10.0.6 |
| Image Processing | SixLabors.ImageSharp | 3.1.12 |
| Containerization | Docker / Docker Compose | 3.8 |
| Queues | Channels (MemoryJobQueue) | System.Threading.Channels |
┌─────────────────────────────────────────────────────────────────────┐
│ WEB LAYER │
│ ┌────────────────┐ ┌────────────────┐ ┌─────────────────┐ │
│ │ AuthController │ │ ImageController│ │ Swagger/OpenAPI │ │
│ └────────────────┘ └────────────────┘ └─────────────────┘ │
│ │ │ │ │
│ Middleware (JWT Auth, CORS, Static Files) │
└─────────────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────────────┐
│ APPLICATION LAYER │
│ ┌────────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ImageProcessor │ │DTOs │ │Interfaces │ │
│ └────────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │ │
│ Services & Use Cases (Business Logic) │
└─────────────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────────────┐
│ DOMAIN LAYER │
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
│ │ImageMatrix │ │ ProcessingJob │ │ SectorPoint │ │
│ │ Route │ │ PixelPoint │ │ Enum JobStatus│ │
│ └───────────────┘ └───────────────┘ └───────────────┘ │
│ │
│ Domain Models │
└─────────────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────────────┐
│ INFRASTRUCTURE LAYER │
│ ┌──────────────┐ ┌───────────────┐ ┌──────────────┐ │
│ │ Repositories │ │ FileSystem │ │ Background │ │
│ │ (EF Core) │ │ Service │ │ Workers │ │
│ └──────────────┘ └───────────────┘ └──────────────┘ │
│ │
│ Data Access & External Services │
└─────────────────────────────────────────────────────────────────────┘
│
┌───────────────┴───────────────┐
▼ ▼
┌───────────────┐ ┌───────────────┐
│ PostgreSQL DB │ │ File Storage │
└───────────────┘ └───────────────┘
1. Initialization (Process 0%)
├── Reading the image into the brightness matrix
└── Input data validation check
2. Vertex Search (Process 5-10%)
├── Calculating rays from the image center at angles
├── Intersection of rays with image boundaries
└── Generating a SectorPoint array
3. Route construction (Process 10-80%)
├── Selecting the next point with the minimum average brightness value
├── Drawing a line on the matrix with contrast adjustment
├── Updating task progress
└── Cycle through until the specified number of steps is reached
4. Rendering the result (Process 80-90%)
├── Normalizing pixel values
├── Adding a sector color palette
└── Saving the final image
5. Data export (Process 90-100%)
├── Saving the route to a text file
├── Writing metadata to the database
└── Returning the results URL
// A line is constructed for each possible endpoint
foreach (var end in possibleEndPoints)
{
// The average brightness of all pixels on the line is calculated
double avgBrightness = CalculateLineAverage(start.Pixel, end.Pixel);
// The point with the minimum average brightness is selected
if (avgBrightness < minValue)
bestEndPoint = end;
}
// The line is added to the route with increased contrast
ApplyContrastAdjustment(start.Pixel, bestEndPoint.Pixel);public static int CalculateOptimalContrast(ImageMatrix image, int stepCount)
{
// Formula: x = 3.2 * (2 * S * sigma) / (N * D)
// Where S = area, σ = standard deviation, N = steps, D = diagonal
double rawX = (6.4 * area * sigma) / (stepCount * diagonal);
return (int)Math.Round(rawX);
}Solves the problem of adapting the algorithm to different image sizes and formats without manually selecting parameters.
private readonly SemaphoreSlim _semaphore = new(maxConcurrency, maxConcurrency);
await _semaphore.WaitAsync(stoppingToken);
_ = Task.Run(() => ProcessJobAsync(jobId, ct), ct);
_semaphore.Release();Prevents server resource exhaustion when processing multiple files simultaneously.
while (retries > 0)
{
try { return new FileStream(...); }
catch when (retries > 0)
{
await Task.Delay(100 * (int)Math.Pow(2, 5 - retries));
retries--;
}
}Provides resilience to temporary file system access issues.
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
CleanupOldFiles(path, options.Value.FileAgeHours);
await Task.Delay(TimeSpan.FromHours(options.Value.CleanupIntervalHours));
}
}Automatically removes files older than the specified age to save disk space.
for (int i = progressStep; i <= request.CountSteps; i += progressStep)
{
start = RouteBuilder.FillRoute(start, ...);
await jobRepo.UpdateProgressAsync(request.JobID, progressPercentage);
// Ability to interrupt or pause processing
}Allows you to pass the current processing status while a long-running operation is running.
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/image/upload |
Upload an image |
| GET | /api/image/job/{jobId} |
Status of a specific task |
| GET | /api/image/jobs |
List of all tasks |
| GET | /api/image/all |
List of all processed images |

