Важное объявление о переносе форумов
  • Привет, Гость!
  • Войти
  • Регистрация
  • Записи
  • Форумы
  • Люди
  • Файлы
  • Работа
  • Технологии
  • Все
  • Новости
  • События
  • Статьи
  • Блоги

Нелинейные преобразования растровых изображений в GDI+

Нелинейные преобразования растровых изображений в GDI+

Algol36
18.03.2011 22:04

Введение

В этой статье я буду в основном описывать построение высокопроизводительного графического приложения под GDI+. Но в конце статьи будет ссылка на вполне себе рабочий компонент. Так что, тех, кого интересует конечный результат, могут не читать последующие пару глав, а перейти к просмотру картинок и скачиванию исходника .




Сразу предупрежу, что я разрабатывал этот компонент просто так, ради интереса, а не для конкретного применения. И предвосхищая вопросы типа «а почему вы не воспользовались технологией/компонентом XXX?», сразу скажу – не воспользовался потому, что мне было интересно это сделать самому, и именно под GDI+. Хотя я имею небольшой опыт и под DirectX/XNA и под WPF, и знаю, что там это сделать легче, но повторюсь – мне не интересно использовать то, что кто- то уже написал до меня.

Кроме того, мне хотелось бы немного рассеять миф про низкую производительность GDI+. Как и следовало ожидать, в большей степени производительность зависит от применяемых алгоритмов, а не от платформы.

Я уже писал про нелинейные преобразования в GDI+, но прошлая статья касалась только векторной графики. Там преобразования довольно тривиальны и сводятся к пересчету координат вершин в пространстве. Для растровых изображений все не так просто.

Зачем это нужно

А зачем собственно нужны нелинейные преобразования? Вариантов применения как минимум два:

Первый – устранение оптических искажений фотографий. Спектр применения широк – от любительской фотографии, до геодезии и обработки спутниковых изображений земли.

Второй – как ни парадоксально, внесение оптических искажений. Зачем? Ну например, для создания «эффекта присутствия», для визуального оформления, в играх, да и просто для развлечения :)

Проблематика

Сначала проясним для себя, что собственно такое преобразования растровых изображений? Линейные (аффинные) преобразования – это такие преобразования, которые описываются формулой:

x’=Mx+c

Где x’- преобразованные координаты,  x – исходные координаты, M – некоторая фиксированная матрица, c – некоторая константа.

К аффинным преобразованиям относится сдвиг, масштабирование и поворот. Все эти типы преобразований уже поддерживаются объектом Graphics, поэтому их реализация не вызывает сложностей.

Сложности вызывают нелинейные преобразования. Они имеют вид, аналогичный приведенной формуле, но только вот матрица М становится зависимой от x. К таким преобразованиям относится перспектива, проекция, отображение плоскости на сферу, эффект линзы, и многие другие.

Если не вдаваться в математику, то на пальцах можно сказать что нелинейные преобразования – это сжатие или растяжение частей изображения. Причем коэффициент сжатия или растяжения – разный в разных частях изображения.

 Поддержки нелинейных преобразований нет в стандартной библиотеке .Net. Но если чего-то нет, а очень хочется, то приходится это писать самому, чем мы собственно и займемся.

Решение в лоб

В принципе,  математическое решение проблемы – тривиально. Изображение – это набор пикселов, которые имеют  свои координаты. Просто подставив координаты пиксела в формулу преобразования – получаем новые координаты пикселов, из которых собственно и сложится изображение. А теории – легко. Но вот на практике такой способ не подходит. Почему? Очень просто – растр это не просто набор пикселов, это непрерывный и регулярный набор пикселов. Если мы просто преобразуем координаты пикселов, то окажется что в какой-то точке результирующего изображения пикселов станет больше, в какой-то – меньше, а в каких-то местах и вообще образуются дыры. В то время как нам необходимо на выходе получить растр, имеющий непрерывную и регулярную сетку из пикселов.

Интерполяция

Для решения вышеозначенной проблемы применяется интерполяция. Интерполяция – это процесс нахождения значений функции (в нашем случае - цвета пиксела) в произвольном месте пространства, по известным значениям функции в некоторых опорных точках.

Мы бы могли преобразовать исходные пикселы, а потом интерполировать значения цвета в «дырках» то есть в местах, где пикселов нет, и таким образом сформировать непрерывный растр.

Это хорошая идея. Но вот незадача – большинство методов интерполяции основано на регулярной сетке. То есть опорные точки должны следовать друг за другом с фиксированным шагом. А у нас-то проблема как раз в том, что опорные пикселы, в общем случае, разбросаны как попало.

В принципе это тоже решаемо – процессом, который называется триангуляцией, и затем применением аффинных преобразований треугольников. Но во-первых это долгие и сложные алгоритмы. Во-вторых, это даст лишь линейную интерполяцию, которая не очень хорошо смотрится. Поэтому мы сразу отказываемся от этого метода.

Обратные преобразования

И вот здесь мы делаем первый финт ушами. Вместо перехода от исходного изображения к результирующему, мы будем делать наоборот – искать исходные пикселы для результирующего пиксела.


Что нам это даст? А вот что: результирующие пикселы составляют непрерывный растр, что нам и требуется. Конечно, когда мы будем искать исходные пикселы, мы будем попадать в «дырки», потому что маловероятно, что обратная функция будет попадать точно в исходные пикселы. Но вот здесь мы как раз применим интерполяцию. Поскольку исходное изображение тоже является растром, то есть имеет регулярную структуру, то сделать интерполяцию цвета в «дырках» между пикселями – задача не очень сложная  и достаточно быстрая.

Второй финт ушами

В общем-то мы бы конечно могли бы интерполировать пикселы исходного изображения. Формулы бикубического (наиболее оптимального) интерполирования используют значения шестнадцати пикселов, вокруг интересующей нас точки. Формула-то не очень сложная, но чувствую, что долго это будет считаться. Да и лень как-то. А если что-то делать лень, нужно найти кого-то, кто это сделает за вас. Этим кем-то оказался объект Graphics.

Идея следующая: поскольку мы заранее знаем, что нам понадобятся интерполированные точки, находящиеся  между исходными пикселями, то ничего нам не мешает найти эти точки заранее, используя масштабирование исходного изображения с помощью стандартных средств GDI+. При загрузке исходного изображения, я сразу увеличиваю его в два раза, используя встроенное бикубическое интерполирование:

        private void CreateImage2X(Image sourceImage)

        {

            if (sourceImage2X != null)

                sourceImage2X.Dispose();

            //создаем увеличинное исходное изображение

            sourceImage2X = new Bitmap(sourceImage.Width * x2, sourceImage.Height * x2, PixelFormat.Format32bppArgb);

            using (Graphics sourceGr2X = Graphics.FromImage(sourceImage2X))

            {

                sourceGr2X.InterpolationMode = InterpolationMode.HighQualityBicubic;

                sourceGr2X.DrawImage(sourceImage, 0, 0, sourceImage2X.Width + 1, sourceImage2X.Height + 1);

            }

        }

 

Исходное изображение не хранится, из-за ненадобности.

При увеличении изображения в два раза, я получаю в четыре раза больше пикселов, чем было в исходном изображении. Это позволит строить нелинейные преобразования с хорошим качеством и с максимальным увеличением – в 2-3 раза. Дальнейшее увеличение будет порождать уже «квадратики». Но на практике, увеличение в 3 и более раз встречается редко.

В принципе, нам ничего не мешает увеличивать исходное изображение и в 3-4 раза, но нужно понимать, что увеличение изображения в n раз увеличивает расход памяти в n^2 раз. Поэтому нужно выбрать баланс между расходами памяти и качеством преобразований.

Небольшое отступление

Вообще, нужно как можно чаще пользоваться встроенными средствами GDI+. Я, честно говоря, не знаю, использует или нет GDI+ аппаратные средства для ускорения расчетов, но факт в том, что встроенные функции GDI+ достаточно быстры, и наврядли вы сможете сделать более производительный алгоритм, тем более на C#.

Буферизация преобразованного изображения

Мы не будем делать нелинейные преобразования в процессе отрисовки исходного изображения на экран. Это было бы слишком долго. Мы сделаем иначе: когда загружается новое изображение или когда пользователь сдвигает или масштабирует изображение – мы будем вырезать из исходного изображения видимую часть, преобразовывать ее и сохранять во временном изображении. Это временное изображение и будем отображать при отрисовке компонента.

Это дает нам во-первых экономию времени и памяти. Если исходное изображение имеет размер 10000*5000 то пользователь, скорее всего, просматривает лишь небольшой фрагмент этого изображения. Следовательно, мы будем обрабатывать тоже лишь небольшой фрагмент, а не всю картинку целиком.

Во-вторых, потенциально, мы вообще можем не хранить все исходное изображение в памяти. Мы можем его хранить на диске, а в нужные моменты времени считывать необходимые нам куски, для формирования временного изображения. Для отрисовки же нам исходное изображение вообще не понадобится.

Такая возможность не реализована в данном компоненте, но такой сценарий возможен.

Небольшое отступление

В этом месте у меня был соблазн использовать такую вещь, как интегральное представление изображения (я уже писал про него вот здесь). Это позволило бы мне почти бесплатно получить эффекты типа размытия (Blur), или возможность быстрого применения различных фильтров (например, дискретных фильтров Хаара или подобных). Но здесь меня остановило то, что разрабатываемый компонент ориентирован на изображения больших размеров. А интегральное представление большого изображения потребовало бы немалых расходов памяти (примерно в два раза больших исходного), и его использование в данном случае было бы наверно не оправданным. Тем не менее, в конце статьи я покажу, что бы можно было бы сделать, если бы эта форма представления изображения все-таки использовалась.

Размер временного изображения

Если внимательно посмотреть на исходный код, то можно заметить, что размер временного изображения больше чем размер видимой области компонента:

        private void CreateVisibleImage()

        {

            int w = (int)Math.Round(ClientSize.Width * Math.Sqrt(2));

            int h = (int)Math.Round(ClientSize.Height * Math.Sqrt(2));

            int s = Math.Max(h, w);

            if (s == 0)

                return;

            visibleImage = new Bitmap(s, s, PixelFormat.Format32bppArgb);

        }

 

Как видим, временное изображение всегда квадратное и его сторона равна максимальной стороне видимой области, умноженной на корень из двух.

Зачем? Очень просто – мы собираемся вращать изображение. Если мы сделаем картинку меньше указанных размеров, то при вращении на экране будут возникать незакрашенные области, с отсутствующим изображением.

Буферизация функции преобразования

Понятно, что функция преобразования может быть довольно сложной. Тем более, что в нашем компоненте мы будем поддерживать произвольную пользовательскую функцию преобразования. А что там будет за функция, и насколько она будет производительна – нам неизвестно. Поэтому мы будем буферизировать функцию преобразования.

        public virtual void BuildFuncBuffer()

        {

            var w = funcBufferSize;

            var h = funcBufferSize;

            funcBuffer = new PointF[h][];

            Parallel.For(0, h, new ParallelOptions(), (y) =>

            {

                funcBuffer[y] = new PointF[w];

                for (int x = 0; x < w; x++)

                    funcBuffer[y][x] = function(2f * x / w - 1f, 2f * y / h - 1f);

            });

        }

 

В этом коде в общем-то все понятно. Мы строим двумерный массив, рассчитываем значения функции преобразования и заносим значения в массив. В дальнейшей работе с изображением, мы будем пользоваться уже готовыми значениями функции в нужных точках из буфера.

Значения координат x и y нормируются и передаются в функцию в интервале от -1 до 1. Возвращаемые функцией преобразованные координаты тоже должны быть нормированы.

Тут сделаем только два замечания. Первое – как видим, мы используем библиотеку Parallel для распараллеливания вычислений. Поскольку  значения функции независимы друг от друга, то вполне корректно вычислять их параллельно.

Второе – размер буфера функции – фиксирован и не совпадает с размером результирующего изображения. Почему? Потому что изображение большое и нам не хотелось бы тратить по 8 байт (именно столько занимает PointF) на каждый пиксел. Да и скорость расчета буфера – тоже важна. Поэтому я взял фиксированный размер буфера функции 1000*1000 пикселов. При его применении, мы будем интерполировать буфер на реальный размер изображения. Таким образом, если видимая часть изображения будет иметь размер более чем 1000 пикселов, то начнутся небольшие «квадратики». Но никто ведь и не обещал полноэкранный режим с  высоким разрешением :) Впрочем, мы можем поменять значение константы funcBufferSize на большее. Это увеличит расход памяти и снизит скорость расчета буфера функции, но на производительность в процессе работы с изображением – не повлияет.

Вращение и масштабирование изображения

Поскольку мы уже имеем механизм применения произвольной функции преобразования к изображению, то вполне себе можем позволить реализовать масштабирование через эту функцию. Каждый раз, при масштабировании, перестраивать буфер функции преобразования – не хочется, поэтому мы применяем масштаб уже в функции построения результирующего изображения:

            //get transformed coordinates

            PointF pF = buff[funcBufferSize * x / w];

            int X = (int)Math.Round((pF.X / zoom + 1f) * w) + dx;

            int Y = (int)Math.Round((pF.Y / zoom + 1f) * h) + dy;

 

здесь buff – буффер функции преобразования, zoom – наш масштаб, dx, dy – сдвиг видимой области относительно исходного изображения.

С масштабированием и сдвигом понятно. А вот с вращением – я сделал иначе. Не очень хотелось  перестраивать изображение для вращения. Поэтому вращение я применяю уже в процессе прорисовки:

        protected override void OnPaint(PaintEventArgs e)

        {

            e.Graphics.TranslateTransform(ClientSize.Width/2f, ClientSize.Height/2f);

            e.Graphics.RotateTransform(rotate);

            e.Graphics.DrawImageUnscaled(visibleImage, -visibleImage.Width / 2, -visibleImage.Height / 2);

        }

 

Таким образом, при вращении, перестройка изображения вообще не происходит, и дается нам практически бесплатно.

Формирование изображения

Собственно это основной метод нашего компонента, который формирует результирующее изображение. Используются стандартные средства для быстрой работы с изображением – через LockBits:

 

        void BuildVisibleImage()

        {

            BitmapData BMPD1 = visibleImage.LockBits();

            BitmapData BMPD2 = sourceImage2X.LockBits();

            int h = visibleImage.Height;

            int w = visibleImage.Width;

            int dx = visibleCenter.X * 2 - (int)w / 2;

            int dy = visibleCenter.Y * 2 - (int)h / 2;

            int h2x = sourceImage2X.Height;

            int w2x = sourceImage2X.Width;

            unsafe

            {

                Parallel.For(0, h, new ParallelOptions(), (y) =>

                {

                    int* p1;

                    int* p2;

                    var buff = funcBuffer[funcBufferSize * y / h];

                    for (int x = 0; x < w; x++)

                    {

                        //get target pixel pointer

                        p1 = (int*)(void*)scan1 + stride1 * y / 4 + x;

                        //get transformed coordinates

                        PointF pF = buff[funcBufferSize * x / w];

                        int X = (int)Math.Round((pF.X / zoom + 1f) * w) + dx;

                        int Y = (int)Math.Round((pF.Y / zoom + 1f) * h) + dy;

                        //get source pixel pointer

                        p2 = (int*)(void*)scan2 + stride2 * Y / 4 + X;

                        //copy source pixel to target position

                        *p1 = *p2;

                    }

                });

            }

        }

 

Здесь приведен почти полный текст метода, с некоторыми несущественными сокращениями.

Суть метода понятна. Перебираются все пикселы результирующего изображения. Для каждого пиксела находятся примерные координаты в исходном изображении (применяя буфер функции преобразования), найденный пиксел копируется в результирующее изображение.

Мы снова  применяем библиотеку Parallel для распараллеливания вычислений, а также немного указателей для оптимизации скорости доступа к памяти.

Встроенные типы преобразований.

Здесь мы наконец-то можем посмотреть на картинки :)

В компоненте реализованы несколько встроенных формул преобразований.

Эффект «Панорама»

Function = (x, y) => new PointF(x / (2 + (float)Math.Atan(x * x + y * y)), y / (2 + (float)Math.Atan(x * x + y * y)));

 

Реализуется отображение изображения на сферу. Зритель находится в центре сферы. Этот эффект часто используется при просмотре панорамных фотографий, для возникновения «эффекта присутствия» у смотрящего.




Здесь изображена сетка, а не настоящая фотография, потому что этот эффект почти не заметен в статичном изображении, его нужно смотреть в динамике.

Небольшое отступление

Здесь стоит заметить, что картинка не в полной мере отображает работу алгоритма, она показывает конечный результат, но по ней нельзя определить скорость работы алгоритма. На самом же деле, эффект работает в реал-тайме, и позволяет почти без задержек сдвигать , масштабировать или вращать изображение.

 

Эффект наклонной плоскости

Function = (x, y) => new PointF(x / 2 * (2 - y), y);

 

Эффект реализует отображение изображения на наклонную плоскость. Собственно все понятно из картинки:




Эффект сферы

Function = (x, y) => new PointF(x * (1 + (float)Math.Atan(x * x + y * y)), y * (1 + (float)Math.Atan(x * x + y * y)));

 

Похоже на эффект панорамы, только наоборот – зритель смотрит на сферу извне:




Эффект линзы

Реализованы две линзы:

Function = (x, y) => (x * x + y * y) < 0.2f ? new PointF(x * (1 + (float)Math.Atan(x * x + y * y)), y * (1 + (float)Math.Atan(x * x + y * y))) : new PointF(x, y);

 

Function = (x, y) => (x * x + y * y) > 0.2f ? new PointF(x * (1 + (float)Math.Atan(x * x + y * y)), y * (1 + (float)Math.Atan(x * x + y * y))) : new PointF(x * 0.5f, y * 0.5f);

 

Картинки:




Первая линза показана на сетке, потому что эффект плохо просматривается на статичном изображении.

В линзах интересно то, что для участков изображения внутри круга применяется одна формула преобразований, я для пикселов вне круга – другая.

Эффект «Капли»

        private void BuildDropsFunction()

        {

            Random rnd = new Random();

            List<PointF> drops = new List<PointF>();

            for (int i = 0; i < 100; i++)

                drops.Add(new PointF((float)rnd.NextDouble() * 2 - 1, (float)rnd.NextDouble() * 2 - 1));

            Function = (x, y) =>

            {

                foreach (var drop in drops)

                {

                    var dx = x - drop.X;

                    var dy = y - drop.Y;

                    var r = dx * dx + dy * dy;

                    if (r < 0.001f)

                    {

                        dx = dx * 20;

                        dy = dy * 20;

                        return new PointF(-(float)Math.Atan(dx) + drop.X, -(float)Math.Atan(dy) + drop.Y);

                    }

                }

                return new PointF(x, y);

            };

        }

 

По видимой области компонента генерируются случайно разбросанные «капли дождя». Каждая капля, по сути, представляет собой маленькую линзу с большой кривизной поверхности:




Эффект «Вода»

Function = (x, y) => new PointF(x + (float)Math.Sin((x * x + y * y) * 50) / 100, y + (float)Math.Sin((x * x + y * y) * 50) / 100);

 

На сетке выглядит так:




Эффект «Матовое стекло»

Центральная часть изображения - без изменений. А по периметру сделан эффект матового стекла:




Думаю не стоит говорить о том, что пользователь, может задать свой, произвольный тип преобразований (кстати, если придумаете оригинальные типы преобразований - не поленитесь, отправьте мне на почту :).

Небольшое отступление

Тут все-таки придется вспомнить немного математику. Смотря на исходники и формулы преобразований, можно заметить, что там часто используются функции типа арктангенсов и тому подобных. Возникает вопрос, почему именно арктангенсы? И вообще, как искать формулы для определенного типа преобразований? Можно конечно вспомнить алгебру и аналитическую геометрию, и честно считать формулы преобразований координат, но я использую более простой (и менее точный) метод.

Для начала посмотрим, какой вид функций соответствует прямому (не искаженному) отображению: очевидно, что это прямая. Функция f(x) = x переводит точки в самих себя. На графике это выглядит так:




Теперь нам хотелось бы, что бы к краям изображения, изображение уменьшалось или увеличивалось. Значит, в центре оно должно выглядеть примерно как на предыдущем графике, а к краям – кривая должна идти вверх или вниз, примерно вот так:




Обратим внимание, что в центре графика, функция имеет асимптоту x'=x. Если вспомнить основные типы функций, то оказывается, что нечетных функций с единичной производной в нуле – не так-то и много. Это синус, тангенс, арксинус, арктангенс и некоторые виды логистических функций. Из них наиболее удобно использовать тангенс и арктангенс.

Обратим внимание на то, что степенные функции (типа параболы или кубической функции) – не подходят. Несмотря на то, что последний график похож на f(x)=x^3 , но кубическая функция имеет производную нуль в нулевой точке. На практике это означает, что в центре изображения будут либо огромные пикселы (бесконечное увеличение), либо «черная дыра» (бесконечное уменьшение). Визуальная ценность и того и другого – сомнительна.

Понятно, что точные формулы (например, отображения плоскости на сферу) – могут быть другими и быть более сложными. Но поскольку нас интересует лишь визуальный эффект, то вполне можем себе позволить пользоваться приблизительными формулами, отображающими лишь тенденции.

Что еще можно было сделать

В некоторых инструментах под  WPF и под Flash есть эффект, который называется  Motion-Blur. По сути, он просто размывает изображение пока пользователь его передвигает. Это создает «эффект движения».

Для Motion-Blur в WPF используется аппаратное ускорение. На самом же деле это совсем не обязательно, и этот эффект можно было бы сделать и в рамках GDI+. Для этого, нам понадобится хранить изображение в интегральной форме (я уже упоминал в середине статьи). В процессе формирования промежуточного изображения мы бы могли оперировать не отдельными пикселами, а усредненными значениями пикселов в некой окресности, что создает размытость. Интегральное представление позволяет сделать эту операцию без существенных временных затрат.

Однако эта возможность не была реализована, поскольку выходит за рамки нелинейных преобразований, и требует большего объема памяти.

Производительность

Компонент тестировался на машине Core2 Quad 2.4GHz (4 ядра), RAM 2 Gb, Windows7.

При размере области просмотра 1024*768 обеспечивается комфортная работа пользователя. Лаги перемещения, вращения и масштабирования изображения почти не заметны.

Производительность просмотра не зависит от размеров исходного изображения и вида функции преобразования. Есть прямая зависимость от размеров области просмотра.

Использование компонента

Представленный компонент можно использовать вместо PictureBox. Он может отображать картинки, позволяя пользователю двигать (левая кнопка мыши), масштабировать (колесико мыши) и вращать изображение (правая кнопка мыши). Ну и конечно, компонент позволяет делать нелинейные преобразования изображения, задавая свою формулу в свойстве Function.

Компонент содержит также полезные события VisibleCenterChanged и RotateAngleChanged. Они срабатывают когда пользователь сдвигает или вращает изображение. Если в обработчик этих событий поместить задание свойства Function, то можно сделать нелинейные преобразования, зависящие от текущего сдвига и вращения изображения.

Полезными также свойствами будут JoinLeftRight И JoinTopBottom. Эти свойства позволяют отображать картинку так, что левый край изображения переходит в правый край. Это полезно для отображения круговых 360 градусных панорам.

Еще могут пригодиться свойства VisibleCenter – точка исходного изображения, куда смотрит в данный момент пользователь. RotateAngle – угол текущего поворота изображения. Zoom – текущий масштаб изображения. Effect – выбор встроенных эффектов (установите в Custom, если вы сами задаете формулу в Function).

Скачать  исходный код и демо можно здесь.

Компонент разработан под FW4. Это обусловлено применением библиотеки Parallel. Но вы можете перекомпилировать  под FW3.5 используя внешнюю dll от Parallel Extensions for FW3.5, либо вообще отказавшись от Parallel, заменив два цикла в коде на обычный цикл for. 

Algol36
18.03.2011 22:04
Комментариев:7 RSS Просмотров:7043
Теги: GDI+, WinForms, PictureBoxEx

Всякая Всячина

Algol36
  • Блог

Облако тегов

3d c# chart csharp cv diagrams dragging fastcоlоrеdtextboх gdi+ google map graphics graphicspath opencv picturebox pictureboxex recognition richtextbox rotate template textbox vector graphics winforms xna zoom векторная графика контурный анализ машинное зрение распознавание образов

Записи

Популярные
  • Sergey Olontsev > Что нового в SQL Server 2014
  • yatajga > Внутри ASP.NET MVC: конвейер обработки запросов, часть четвёртая (фабрика контроллеров, введение)
  • SergeyT. > О книге Боба Мартина "Чистый код"
  • SergeyT. > О комментариях. Часть 2
  • yatajga > Веб-приложения реального времени. Веб-сокеты, IIS 8, библиотека SignalR и их использование в приложениях ASP.NET. Часть третья, простой пример использования веб-сокетов в приложении ASP.NET.
  • yatajga > Веб-приложения реального времени. Веб-сокеты, IIS 8, библиотека SignalR и их использование в приложениях ASP.NET. Часть первая, веб-сокеты.
  • yatajga > Внутри ASP.NET MVC: конвейер обработки запросов, часть четвёртая (фабрика контроллеров, класс ControllerBuild­­er)
  • SergeyT. > О пользовательски­х преобразованиях типов
  • mr_squall > How to use Instagram API via c#
  • yatajga > Веб-приложения реального времени. Веб-сокеты, IIS 8, библиотека SignalR и их использование в приложениях ASP.NET. Часть вторая, веб-сокеты и IIS 8.
Все популярные записи
Обсуждаемые
  • Elisy > Внешнее воздействие на веб-клиент 1С:Предприятие
Все обсуждаемые записи

Блоги

Новые
  • Николай Селютин> SharePoint: Заметки разработчика
  • Антон Герасимюк> Test
  • ahrimanb> HPC & Cloud
  • AiZee> Разработка в Sharepoint и .NET
  • slavanetdevelop­er> Разработка Microsoft.NET приложений
  • pa> Блог о .NET
  • Quarta Technologies> ВСТРАИВАЕМЫЕ ТЕХНОЛОГИИ 2013. Современные программные и аппаратные решения
  • Fortune> "SharePoint 2010 в простых картинках" онлайн книга для Пользователей
  • mbakirov> Mbakirov1
  • Andrew> Наблюдая за ИТ
Обсуждаемые
  • Русский MSDN> Новости Русского MSDN
  • XaocCPS> Владимир Юнев
  • Михаил Черномордиков> Mikhail Chernomordikov [MSFT]
  • mihailik> Олег Михайлик
  • ceo> Нотатник Вiктора Шатохiна [MSFT]
  • gaidar> Гайдар Магдануров - Блог
  • Alexander Lozhechkin [MSFT]> Alexander Lozhechkin
  • sergun> Sergey Zwezdin
  • sashaeve> Блог Microsoft .NET User Group Винница
  • agladkik> Andrey Gladkikh: Microsoft Dynamics
О сайте   Свяжитесь с нами   Версия для печати
Работает на .NET Forge CMS  |  Хостинг на Parking.Ru